An toàn chuỗi List.Add ()


87

Tôi hiểu rằng nói chung Danh sách không an toàn cho chuỗi, tuy nhiên, có gì sai khi chỉ cần thêm các mục vào danh sách nếu chuỗi không bao giờ thực hiện bất kỳ thao tác nào khác trên danh sách (chẳng hạn như duyệt qua nó)?

Thí dụ:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

3
Bản sao chính xác của an toàn luồng Danh sách <T>
— JK.

Tôi đã từng sử dụng một Danh sách <T> chỉ để thêm các đối tượng mới từ nhiều tác vụ chạy song song. Đôi khi, rất hiếm khi lặp lại danh sách sau khi tất cả các nhiệm vụ đã hoàn thành, nó kết thúc với một bản ghi trống, điều này, nếu không có chuỗi bổ sung nào được tham gia, thì điều này thực tế sẽ không thể xảy ra. Tôi đoán rằng, khi danh sách đang phân bổ lại nội bộ các phần tử của nó để mở rộng, bằng cách nào đó, một luồng khác đã làm rối tung nó bằng cách cố gắng thêm một đối tượng khác. Vì vậy, không phải là một ý kiến ​​hay khi làm như vậy!
— osmiumbin

Chính xác những gì tôi hiện đang thấy @osmiumbin liên quan đến một đối tượng không thể giải thích được là rỗng khi chỉ cần thêm từ nhiều chủ đề. Cám ơn vì đã xác nhận.
— Blackey

Câu trả lời:


74

Phía sau hậu trường có rất nhiều thứ xảy ra, bao gồm phân bổ lại bộ đệm và sao chép các yếu tố. Mã đó sẽ gây ra nguy hiểm. Rất đơn giản, không có thao tác nguyên tử nào khi thêm vào danh sách, ít nhất thuộc tính "Độ dài" cần được cập nhật và mục cần được đưa vào đúng vị trí và (nếu có một biến riêng) chỉ mục cần để được cập nhật. Nhiều chủ đề có thể thay đổi lẫn nhau. Và nếu cần phải tăng trưởng thì còn rất nhiều điều nữa đang diễn ra. Nếu một cái gì đó đang ghi vào danh sách thì không có gì khác nên đọc hoặc ghi vào nó.

Trong .NET 4.0, chúng tôi có các bộ sưu tập đồng thời, được an toàn theo luồng thủ công và không yêu cầu khóa.


Điều đó có ý nghĩa hoàn hảo, tôi chắc chắn sẽ xem xét các bộ sưu tập Đồng thời mới cho điều này. Cảm ơn bạn.
— e36M3

11
Lưu ý rằng không có loại cài sẵn ConcurrentList. Có các túi, từ điển, ngăn xếp, hàng đợi, v.v. đồng thời, nhưng không có danh sách.
— LukeH

11

Cách tiếp cận hiện tại của bạn không an toàn cho chuỗi - tôi khuyên bạn nên tránh điều này hoàn toàn - vì về cơ bản bạn thực hiện chuyển đổi dữ liệu PLINQ có thể là một cách tiếp cận tốt hơn (tôi biết đây là một ví dụ đơn giản nhưng cuối cùng bạn đang chiếu mỗi giao dịch sang một trạng thái khác " vật).

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

Tôi đã trình bày một ví dụ đơn giản hóa quá mức để nhấn mạnh khía cạnh của List.Add mà tôi quan tâm. Thực tế Parallel.Foreach của tôi sẽ thực hiện rất nhiều công việc và sẽ không phải là một chuyển đổi dữ liệu đơn giản. Cảm ơn.
— e36M3

4
các bộ sưu tập đồng thời có thể làm tê liệt hiệu suất song song của bạn nếu được sử dụng không cần thiết - một điều khác bạn có thể làm là sử dụng một mảng có kích thước cố định và sử dụng Parallel.Foreachquá tải lấy chỉ mục - trong trường hợp đó mỗi luồng đang thao tác một mục nhập mảng khác nhau và bạn nên an toàn.
— BrokenGlass

6

Nó không phải là một điều vô lý để hỏi. Có những trường hợp các phương pháp có thể gây ra các vấn đề an toàn luồng kết hợp với các phương pháp khác là an toàn nếu chúng là phương pháp duy nhất được gọi.

Tuy nhiên, đây rõ ràng không phải là trường hợp của nó, khi bạn xem xét mã được hiển thị trong phản xạ:

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

Ngay cả khi EnsureCapacitybản thân nó đã là threadsafe (và chắc chắn là không), đoạn mã trên rõ ràng sẽ không phải là threadsafe, khi xem xét khả năng các cuộc gọi đồng thời đến toán tử gia tăng gây ra ghi sai.

Khóa, sử dụng ConcurrentList hoặc có thể sử dụng hàng đợi không có khóa làm nơi nhiều luồng ghi vào và đọc từ đó - trực tiếp hoặc bằng cách điền vào danh sách - sau khi họ đã hoàn thành công việc của mình (tôi giả sử rằng nhiều lần ghi đồng thời theo sau là đọc một luồng là mẫu của bạn ở đây, dựa trên câu hỏi của bạn, vì nếu không, tôi không thể thấy điều kiện như thế nào mà Addphương pháp duy nhất được gọi có thể sử dụng).


6

Nếu bạn muốn sử dụng List.addtừ nhiều luồng và không quan tâm đến thứ tự, thì bạn có thể không cần khả năng lập chỉ mục của một Listluồng, và nên sử dụng một số bộ sưu tập đồng thời có sẵn để thay thế.

Nếu bạn bỏ qua lời khuyên này và chỉ làm add, bạn có thể làm cho addchuỗi an toàn nhưng theo thứ tự không thể đoán trước như thế này:

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

Nếu bạn chấp nhận thứ tự không thể đoán trước này, rất có thể bạn như đã đề cập trước đó không cần một bộ sưu tập có thể thực hiện lập chỉ mục như trong someList[i].


5

Điều này sẽ gây ra sự cố, vì Danh sách được xây dựng trên một mảng và không an toàn cho chuỗi, bạn có thể lấy chỉ mục ra khỏi giới hạn ngoại lệ hoặc một số giá trị ghi đè các giá trị khác, tùy thuộc vào vị trí của chuỗi. Về cơ bản, đừng làm điều đó.

Có nhiều vấn đề tiềm ẩn ... Chỉ cần không. Nếu bạn cần một bộ sưu tập an toàn chuỗi, hãy sử dụng khóa hoặc một trong các bộ sưu tập System.Collections.Concurrent.


5

Tôi đã giải quyết vấn đề của mình bằng cách sử dụng ConcurrentBag<T>thay vì List<T>như thế này:

ConcurrentBag<object> list = new ConcurrentBag<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

2

Có gì sai khi chỉ cần thêm các mục vào danh sách nếu các chủ đề không bao giờ thực hiện bất kỳ thao tác nào khác trên danh sách?

Câu trả lời ngắn gọn: có.

Câu trả lời dài: chạy chương trình bên dưới.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

@royi bạn đang chạy cái này trên máy lõi đơn?
— Bas Smit

Xin chào, tôi nghĩ có vấn đề với ví dụ này vì nó đang đặt AutoResetEvent bất cứ khi nào nó tìm thấy số 1000. Bởi vì nó có thể xử lý các chủ đề này bất cứ khi nào nó muốn, nó có thể đạt đến 1000 trước khi nó đến 999 chẳng hạn. Nếu bạn thêm Console.WriteLine trong phương thức AddTol thì bạn sẽ thấy rằng việc đánh số không theo thứ tự.
— Dave Walker

@dave, nó thiết lập sự kiện khi tôi == 0
— Bas Smit

2

Như những người khác đã nói, bạn có thể sử dụng các tập hợp đồng thời từ System.Collections.Concurrentkhông gian tên. Nếu bạn có thể sử dụng một trong số đó, điều này được ưu tiên.

Nhưng nếu bạn thực sự muốn một danh sách vừa được đồng bộ hóa, bạn có thể xem SynchronizedCollection<T>-Class in System.Collections.Generic.

Lưu ý rằng bạn phải bao gồm lắp ráp System.ServiceModel, đó cũng là lý do tại sao tôi không thích nó cho lắm. Nhưng đôi khi tôi sử dụng nó.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.