Không có Danh sách đồng thời <T> trong .Net 4.0?


198

Tôi đã rất vui mừng khi thấy System.Collections.Concurrentkhông gian tên mới trong .Net 4.0, khá hay! Tôi đã nhìn thấy ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBagBlockingCollection.

Một thứ dường như bị mất tích một cách bí ẩn là a ConcurrentList<T>. Tôi có phải tự viết nó ra (hoặc lấy nó ra khỏi web :)) không?

Tôi có thiếu một cái gì đó rõ ràng ở đây?



4
@RodrigoReis, ConcurrencyBag <T> là một bộ sưu tập không có thứ tự, trong khi Danh sách <T> được đặt hàng.
Adam Calvet Bohl

4
Làm thế nào bạn có thể có một bộ sưu tập được đặt hàng trong một môi trường đa luồng? Bạn sẽ không bao giờ có quyền kiểm soát chuỗi các yếu tố, theo thiết kế.
Jeremy Holovacs

Sử dụng Khóa thay thế
Erik Bergstedt

có một tệp có tên ThreadSafeList.cs trong mã nguồn dotnet trông rất giống một số mã dưới đây. Nó cũng sử dụng ReaderWriterLockSlim và đang cố gắng tìm hiểu tại sao lại sử dụng khóa đó thay vì khóa đơn giản (obj)?
colin lamarre

Câu trả lời:


166

Tôi đã thử lại một lúc (cũng: trên GitHub ). Việc thực hiện của tôi có một số vấn đề, mà tôi sẽ không nhận được ở đây. Hãy để tôi nói với bạn, quan trọng hơn, những gì tôi học được.

Thứ nhất, không có cách nào bạn có thể thực hiện đầy đủ IList<T> điều đó là không khóa và an toàn cho chuỗi. Cụ thể, các thao tác chèn và xóa ngẫu nhiên sẽ không hoạt động, trừ khi bạn cũng quên truy cập ngẫu nhiên O (1) (trừ khi bạn "gian lận" và chỉ sử dụng một số loại danh sách được liên kết và để cho việc lập chỉ mục hút).

Những gì tôi nghĩ có thể đáng giá là một tập hợp con giới hạn, an toàn của luồng IList<T>: cụ thể, một tập hợp cho phép Addvà cung cấp quyền truy cập chỉ đọc ngẫu nhiên theo chỉ mục (nhưng không Insert,RemoveAt vv, và cũng không ngẫu nhiên ghi truy cập).

Đây là mục tiêu thực hiện của tôiConcurrentList<T> . Nhưng khi tôi kiểm tra hiệu năng của nó trong các kịch bản đa luồng, tôi thấy rằng chỉ cần đồng bộ hóa thêm vào List<T>là nhanh hơn . Về cơ bản, thêm vào một List<T>là nhanh như chớp; độ phức tạp của các bước tính toán liên quan là rất nhỏ (tăng chỉ số và gán cho một phần tử trong một mảng; đó thực sự là nó ). Bạn sẽ cần một tấn ghi đồng thời để xem bất kỳ loại tranh chấp khóa nào về điều này; và thậm chí sau đó, hiệu suất trung bình của mỗi lần viết vẫn sẽ đánh bại việc triển khai không khóa đắt tiền hơn mặc dù ConcurrentList<T>.

Trong trường hợp tương đối hiếm mà mảng nội bộ của danh sách cần thay đổi kích thước, bạn phải trả một khoản chi phí nhỏ. Vì vậy, cuối cùng tôi đã kết luận rằng đây là một kịch bản thích hợp trong đó một ConcurrentList<T>loại bộ sưu tập chỉ thêm sẽ có ý nghĩa: khi bạn muốn được đảm bảo chi phí thấp cho việc thêm một yếu tố vào mỗi cuộc gọi (vì vậy, trái với mục tiêu hiệu suất được khấu hao).

Nó đơn giản không phải là một lớp học hữu ích như bạn nghĩ.


52
Và nếu bạn cần một cái gì đó tương tự như List<T>sử dụng đồng bộ hóa cũ, dựa trên màn hình, sẽ SynchronizedCollection<T>bị ẩn trong BCL: msdn.microsoft.com/en-us/l Library / ms668265.aspx
LukeH

8
Một bổ sung nhỏ: sử dụng tham số Công cụ xây dựng để tránh (càng nhiều càng tốt) kịch bản thay đổi kích thước.
Henk Holterman

2
Kịch bản lớn nhất mà ConcurrentListchiến thắng sẽ là khi không có nhiều hoạt động bổ sung vào danh sách, nhưng có nhiều độc giả đồng thời. Người ta có thể giảm chi phí của độc giả xuống một rào cản bộ nhớ duy nhất (và loại bỏ ngay cả khi độc giả không quan tâm đến dữ liệu hơi cũ).
supercat

2
@Kevin: Thật là tầm thường khi xây dựng ConcurrentList<T>theo kiểu sao cho độc giả được đảm bảo nhìn thấy trạng thái nhất quán mà không cần bất kỳ khóa nào, với chi phí tương đối nhẹ. Khi danh sách mở rộng từ ví dụ kích thước 32 đến 64, hãy giữ mảng size-32 và tạo một mảng size-64 mới. Khi thêm từng mục trong 32 mục tiếp theo, hãy đặt nó vào vị trí 32-63 của mảng mới và sao chép một mục cũ từ mảng size-32 sang mảng mới. Cho đến khi mục thứ 64 được thêm vào, người đọc sẽ tìm trong mảng size-32 cho các mục 0-31 và trong mảng size-64 cho các mục 32-63.
supercat

2
Khi mục thứ 64 được thêm vào, mảng size-32 sẽ vẫn hoạt động để tìm nạp các mục 0-31, nhưng người đọc sẽ không còn cần phải sử dụng nó nữa. Họ có thể sử dụng mảng size-64 cho tất cả các mục 0-63 và mảng size-128 cho các mục 64-127. Chi phí chung của việc chọn một trong hai mảng sẽ sử dụng, cộng với rào cản bộ nhớ nếu muốn, sẽ thấp hơn chi phí của khóa đọc-ghi hiệu quả nhất có thể tưởng tượng được. Người viết có lẽ nên sử dụng khóa (không có khóa là có thể, đặc biệt là nếu người ta không ngại tạo một đối tượng mới với mỗi lần chèn, nhưng khóa phải rẻ.
supercat

38

Bạn sẽ sử dụng một Danh sách đồng thời để làm gì?

Khái niệm về một thùng chứa Truy cập ngẫu nhiên trong một thế giới có luồng không hữu ích như nó có thể xuất hiện. Tuyên bố

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

như một toàn thể vẫn sẽ không được an toàn chủ đề.

Thay vì tạo một Danh sách đồng thời, hãy thử xây dựng các giải pháp với những gì có. Các lớp phổ biến nhất là ConcurrencyBag và đặc biệt là BlockingCollection.


Điểm tốt. Tuy nhiên, những gì tôi đang làm là một chút trần tục hơn. Tôi chỉ đang cố gắng gán concienBag <T> vào IList <T>. Tôi có thể chuyển tài sản của mình sang một số <T> IE, nhưng sau đó tôi không thể. Thêm công cụ vào đó.
Alan

1
@Alan: Không có cách nào để thực hiện điều đó mà không khóa danh sách. Vì Monitordù sao bạn cũng có thể sử dụng để làm điều đó, không có lý do gì cho một danh sách đồng thời.
Billy ONeal

6
@dcp - vâng, cái này vốn không an toàn. ConcurrencyDixi có các phương thức đặc biệt thực hiện điều này trong một hoạt động nguyên tử, như AddOrUpdate, GetOrAdd, TryUpdate, v.v. Họ vẫn có ContainsKey vì đôi khi bạn chỉ muốn biết liệu khóa có ở đó không mà không sửa đổi từ điển (nghĩ
Hashset

3
@dcp - ContainsKey tự nó là chủ đề an toàn, ví dụ của bạn (không phải ContainsKey!) Chỉ có điều kiện cuộc đua vì bạn thực hiện cuộc gọi thứ hai tùy thuộc vào quyết định đầu tiên, có thể tại thời điểm đó đã hết hạn.
Zarat

2
Henk, tôi không đồng ý. Tôi nghĩ rằng có một kịch bản đơn giản mà nó có thể rất hữu ích. Chủ đề công nhân viết trong đó sẽ giao diện người dùng đọc và cập nhật giao diện tương ứng. Nếu bạn muốn thêm mục theo cách được sắp xếp, nó sẽ yêu cầu ghi truy cập ngẫu nhiên. Bạn cũng có thể sử dụng ngăn xếp và chế độ xem dữ liệu nhưng bạn sẽ phải duy trì 2 bộ sưu tập :-(.
Eric Ouellet

19

Với tất cả sự tôn trọng đối với các câu trả lời tuyệt vời đã được cung cấp, có những lúc tôi chỉ muốn một IList an toàn theo chủ đề. Không có gì tiên tiến hoặc ưa thích. Hiệu suất là quan trọng trong nhiều trường hợp nhưng đôi khi đó không phải là một mối quan tâm. Vâng, luôn luôn có những thách thức mà không có phương pháp như "TryGetValue", v.v., nhưng hầu hết các trường hợp tôi chỉ muốn một cái gì đó mà tôi có thể liệt kê mà không cần phải lo lắng về việc khóa tất cả mọi thứ. Và vâng, ai đó có thể có thể tìm thấy một số "lỗi" trong quá trình triển khai của tôi có thể dẫn đến bế tắc hoặc một cái gì đó (tôi cho là vậy) nhưng hãy thành thật: Khi nói đến đa luồng, nếu bạn không viết mã chính xác, nó sẽ viết mã chính xác dù sao cũng sẽ bế tắc. Với ý nghĩ đó, tôi đã quyết định thực hiện một triển khai đồng thời đơn giản cung cấp các nhu cầu cơ bản này.

Và với giá trị của nó: Tôi đã thực hiện một thử nghiệm cơ bản về việc thêm 10.000.000 mục vào Danh sách thông thường và Danh sách đồng thời và kết quả là:

Danh sách kết thúc sau: 7793 mili giây. Đồng thời kết thúc sau: 8064 mili giây.

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}

5
OK, câu trả lời cũ nhưng vẫn: RemoveAt(int index)không bao giờ an toàn cho chuỗi, Insert(int index, T item)chỉ an toàn cho chỉ mục == 0, việc trả về IndexOf()ngay lập tức đã lỗi thời, v.v. Thậm chí đừng bắt đầu về this[int].
Henk Holterman

2
Và bạn không cần và không muốn ~ Finalizer ().
Henk Holterman

2
Bạn nói rằng bạn đã từ bỏ việc ngăn chặn khả năng bế tắc - và một lần duy nhất ReaderWriterLockSlimcó thể được thực hiện để bế tắc một cách dễ dàng bằng cách sử dụng EnterUpgradeableReadLock()đồng thời. Tuy nhiên, bạn không sử dụng nó, bạn không làm cho khóa có thể truy cập được ra bên ngoài và bạn không ví dụ như gọi một phương thức nhập khóa ghi trong khi giữ khóa đọc, do đó, việc sử dụng lớp của bạn sẽ không gây ra bế tắc nữa có khả năng
Eugene Beresovsky

1
Giao diện không đồng thời không thích hợp để truy cập đồng thời. Ví dụ như sau đây không phải là nguyên tử var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. Nói chung, bất kỳ kết hợp đọc-ghi có thể dẫn bạn vào rắc rối sâu sắc khi được thực hiện đồng thời. Đó là lý do tại sao các cấu trúc dữ liệu đồng thời thường cung cấp các phương thức cho những thứ đó, như ConcurrentDictionarycủa AddOrGetNB. Hằng số của bạn (và không cần thiết bởi vì các thành viên đã được đánh dấu như vậy bởi sự lặp lại của dấu gạch dưới) this..
Eugene Beresovsky

1
Cảm ơn anh. Tôi là một người dùng nặng của .NET Reflector, đặt "cái này." trên tất cả các trường không tĩnh. Như vậy, tôi đã phát triển để thích giống nhau. Về giao diện không đồng thời này không phù hợp: Bạn hoàn toàn đúng khi cố gắng thực hiện nhiều hành động chống lại việc triển khai của tôi có thể trở nên không đáng tin cậy. Nhưng yêu cầu ở đây chỉ đơn giản là các hành động đơn lẻ (thêm, hoặc xóa, hoặc xóa hoặc liệt kê) có thể được thực hiện mà không làm hỏng bộ sưu tập. Về cơ bản, nó loại bỏ sự cần thiết phải đặt các câu lệnh khóa xung quanh mọi thứ.
Gian hàng Brian

11

ConcurrentList(như một mảng có thể thay đổi kích thước, không phải là một danh sách được liên kết) không dễ viết với các hoạt động không chặn. API của nó không dịch tốt sang phiên bản "đồng thời".


12
Nó không chỉ khó viết, thậm chí còn khó để tìm ra một giao diện hữu ích.
CodeInChaos

11

Lý do tại sao không có Danh sách đồng thời là vì về cơ bản nó không thể được viết. Lý do tại sao một số hoạt động quan trọng trong IList dựa vào các chỉ số và điều đó hoàn toàn không hiệu quả. Ví dụ:

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

Hiệu ứng mà tác giả sẽ theo sau là chèn "dog" trước "cat", nhưng trong một môi trường đa luồng, bất cứ điều gì cũng có thể xảy ra với danh sách giữa hai dòng mã này. Ví dụ, một luồng khác có thể làm list.RemoveAt(0), chuyển toàn bộ danh sách sang trái, nhưng điều quan trọng nhất là cat Index sẽ không thay đổi. Tác động ở đây là Inserthoạt động sẽ thực sự đặt "con chó" sau con mèo chứ không phải trước nó.

Một số triển khai mà bạn thấy được cung cấp là "câu trả lời" cho câu hỏi này rất có ý nghĩa, nhưng như các chương trình trên, chúng không mang lại kết quả đáng tin cậy. Nếu bạn thực sự muốn ngữ nghĩa giống như danh sách trong một môi trường đa luồng, bạn không thể đến đó bằng cách đặt các khóa bên trong các phương thức thực hiện danh sách. Bạn phải đảm bảo rằng bất kỳ chỉ mục nào bạn sử dụng đều sống hoàn toàn bên trong bối cảnh của khóa. Kết quả cuối cùng là bạn có thể sử dụng Danh sách trong môi trường đa luồng với khóa đúng, nhưng bản thân danh sách không thể tồn tại trong thế giới đó.

Nếu bạn nghĩ rằng bạn cần một danh sách đồng thời, thực sự chỉ có hai khả năng:

  1. Những gì bạn thực sự cần là một đồng thời
  2. Bạn cần tạo bộ sưu tập của riêng mình, có thể được triển khai với Danh sách và kiểm soát đồng thời của riêng bạn.

Nếu bạn có một concallelBag và đang ở vị trí mà bạn cần vượt qua nó với tư cách là một IList, thì bạn có một vấn đề, bởi vì phương thức bạn đang gọi đã chỉ định rằng họ có thể cố gắng làm điều gì đó như tôi đã làm ở trên với con mèo & chó. Trong hầu hết các thế giới, điều đó có nghĩa là phương thức bạn gọi đơn giản là không được xây dựng để hoạt động trong môi trường đa luồng. Điều đó có nghĩa là bạn hoặc cấu trúc lại nó sao cho đúng hoặc nếu không thể, bạn sẽ phải xử lý nó rất cẩn thận. Bạn gần như chắc chắn sẽ được yêu cầu tạo bộ sưu tập của riêng mình bằng các khóa riêng và gọi phương thức vi phạm trong một khóa.


5

Trong trường hợp số lần đọc vượt quá số lần ghi, hoặc (tuy nhiên thường xuyên) việc ghi không đồng thời , cách tiếp cận sao chép trên ghi có thể phù hợp.

Việc thực hiện dưới đây là

  • không khóa
  • cực kỳ nhanh để đọc đồng thời , ngay cả khi các sửa đổi đồng thời đang diễn ra - bất kể chúng mất bao lâu
  • bởi vì "ảnh chụp nhanh" là bất biến, nguyên tử không khóa là có thể, tức là var snap = _list; snap[snap.Count - 1];sẽ không bao giờ (tốt, ngoại trừ một danh sách trống tất nhiên), và bạn cũng nhận được liệt kê an toàn theo chủ đề với ngữ nghĩa chụp nhanh miễn phí .. làm thế nào tôi YÊU sự bất biến!
  • triển khai rộng rãi , áp dụng cho mọi cấu trúc dữ liệumọi loại sửa đổi
  • Chết đơn giản , dễ kiểm tra, gỡ lỗi, xác minh bằng cách đọc mã
  • có thể sử dụng trong .Net 3.5

Để sao chép trên ghi hoạt động, bạn phải giữ cấu trúc dữ liệu của mình một cách hiệu quả bất biến , tức là không ai được phép thay đổi chúng sau khi bạn cung cấp chúng cho các luồng khác. Khi bạn muốn sửa đổi, bạn

  1. nhân bản cấu trúc
  2. sửa đổi trên bản sao
  3. trao đổi nguyên tử trong tham chiếu đến bản sao được sửa đổi

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

Sử dụng

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

Nếu bạn cần hiệu năng cao hơn, nó sẽ giúp làm sáng tỏ phương thức, ví dụ: tạo một phương thức cho mọi loại sửa đổi (Thêm, Xóa, ...) bạn muốn và mã cứng các con trỏ hàm clonerop.

NB # 1 Bạn có trách nhiệm đảm bảo không ai sửa đổi cấu trúc dữ liệu bất biến (được cho là). Không có gì chúng ta có thể làm trong một triển khai chung để ngăn chặn điều đó, nhưng khi chuyên môn hóa List<T>, bạn có thể bảo vệ chống lại sửa đổi bằng List.AsReadOnly ()

NB # 2 Hãy cẩn thận về các giá trị trong danh sách. Cách tiếp cận sao chép trên chỉ bảo vệ tư cách thành viên danh sách của họ, nhưng nếu bạn không đặt chuỗi, nhưng một số đối tượng có thể thay đổi khác trong đó, bạn phải quan tâm đến an toàn luồng (ví dụ: khóa). Nhưng đó là trực giao với giải pháp này và ví dụ: khóa các giá trị có thể thay đổi có thể dễ dàng sử dụng mà không gặp vấn đề gì. Bạn chỉ cần nhận thức được nó.

NB # 3 Nếu cấu trúc dữ liệu của bạn rất lớn và bạn sửa đổi nó thường xuyên, phương pháp sao chép toàn bộ ghi có thể bị cấm cả về mức tiêu thụ bộ nhớ và chi phí sao chép CPU liên quan. Trong trường hợp đó, bạn có thể muốn sử dụng Bộ sưu tập bất biến của MS thay thế.


3

System.Collections.Generic.List<t>đã là chủ đề an toàn cho nhiều người đọc. Cố gắng làm cho nó chủ đề an toàn cho nhiều nhà văn sẽ không có ý nghĩa. (Vì lý do Henk và Stephen đã đề cập)


Bạn không thể thấy một kịch bản mà tôi có thể có 5 luồng thêm vào danh sách? Bằng cách này, bạn có thể thấy danh sách tích lũy hồ sơ ngay cả trước khi tất cả chúng chấm dứt.
Alan

9
@Alan - đó sẽ là một concienQueue, ConcurrencyStack hoặc ConcurrencyBag. Để hiểu ý nghĩa của một Danh sách đồng thời, bạn nên cung cấp một trường hợp sử dụng trong đó các lớp có sẵn là không đủ. Tôi không thấy lý do tại sao tôi muốn truy cập được lập chỉ mục khi các yếu tố tại các chỉ số có thể thay đổi ngẫu nhiên thông qua việc loại bỏ đồng thời. Và đối với việc đọc "bị khóa", bạn đã có thể chụp ảnh nhanh các lớp đồng thời hiện có và đưa chúng vào danh sách.
Zarat

Bạn nói đúng - Tôi không muốn truy cập được lập chỉ mục. Tôi thường sử dụng IList <T> làm proxy cho một IEnumerable mà tôi có thể .Thêm (T) các phần tử mới. Đó thực sự là câu hỏi đến từ đâu.
Alan

@Alan: Sau đó, bạn muốn một hàng đợi, không phải là một danh sách.
Billy ONeal

3
Tôi nghĩ rằng bạn là sai. Nói: an toàn cho nhiều người đọc không có nghĩa là bạn không thể viết cùng một lúc. Viết cũng có nghĩa là xóa và bạn sẽ gặp lỗi nếu bạn xóa trong khi lặp lại trong đó.
Eric Ouellet

2

Một số người che giấu một số điểm hàng hóa (và một số suy nghĩ của tôi):

  • Nó có thể trông giống điên rồ khi không thể truy cập ngẫu nhiên (bộ chỉ mục) nhưng với tôi nó có vẻ ổn. Bạn chỉ cần nghĩ rằng có nhiều phương pháp trên các bộ sưu tập đa luồng có thể thất bại như Indexer và Delete. Bạn cũng có thể xác định hành động thất bại (dự phòng) cho trình truy cập ghi như "fail" hoặc đơn giản là "thêm vào cuối".
  • Không phải vì nó là một bộ sưu tập đa luồng mà nó sẽ luôn được sử dụng trong bối cảnh đa luồng. Hoặc nó cũng chỉ có thể được sử dụng bởi một nhà văn và một người đọc.
  • Một cách khác để có thể sử dụng bộ chỉ mục một cách an toàn có thể là bọc các hành động vào một khóa của bộ sưu tập bằng cách sử dụng gốc của nó (nếu được công khai).
  • Đối với nhiều người, việc tạo một rootLock có thể nhìn thấy được là "Thực hành tốt". Tôi không chắc chắn 100% về điểm này bởi vì nếu nó bị ẩn, bạn sẽ loại bỏ rất nhiều tính linh hoạt cho người dùng. Chúng ta luôn phải nhớ rằng lập trình đa luồng không dành cho bất kỳ ai. Chúng tôi không thể ngăn chặn mọi loại sử dụng sai.
  • Microsoft sẽ phải thực hiện một số công việc và xác định một số tiêu chuẩn mới để giới thiệu cách sử dụng hợp lý bộ sưu tập Đa luồng. Đầu tiên, IEnumerator không nên có moveNext mà nên có GetNext trả về true hoặc false và lấy tham số loại T (theo cách này, phép lặp sẽ không bị chặn nữa). Ngoài ra, Microsoft đã sử dụng "sử dụng" nội bộ trong foreach nhưng đôi khi sử dụng IEnumerator trực tiếp mà không gói nó bằng "bằng cách sử dụng" (một lỗi trong chế độ xem bộ sưu tập và có thể ở nhiều nơi hơn) - Việc sử dụng IEnumerator là một ưu tiên được khuyến nghị bởi Microsoft. Lỗi này loại bỏ tiềm năng tốt cho trình lặp an toàn ... Trình lặp đó khóa bộ sưu tập trong hàm tạo và mở khóa trên phương thức Vứt bỏ của nó - cho phương thức chặn foreach.

Đó không phải là một câu trả lời. Đây chỉ là những bình luận không thực sự phù hợp với một địa điểm cụ thể.

... Kết luận của tôi, Microsoft phải thực hiện một số thay đổi sâu sắc đối với "foreach" để làm cho bộ sưu tập MultiThreaded dễ sử dụng hơn. Ngoài ra, nó phải tuân theo các quy tắc sử dụng IEnumerator riêng. Cho đến lúc đó, chúng ta có thể viết MultiThreadList một cách dễ dàng sẽ sử dụng trình lặp chặn nhưng điều đó sẽ không tuân theo "IList". Thay vào đó, bạn sẽ phải xác định giao diện "IListPersonnal" riêng có thể không thành công khi "chèn", "xóa" và trình truy cập ngẫu nhiên (bộ chỉ mục) mà không có ngoại lệ. Nhưng ai sẽ muốn sử dụng nó nếu nó không chuẩn?


Người ta có thể dễ dàng viết một ConcurrentOrderedBag<T>cái sẽ bao gồm triển khai chỉ đọc IList<T>, nhưng cũng sẽ cung cấp một int Add(T value)phương pháp hoàn toàn an toàn cho chuỗi . Tôi không thấy lý do tại sao ForEachcần phải thay đổi. Mặc dù Microsoft không nói rõ ràng như vậy, nhưng thực tế của họ cho thấy rằng IEnumerator<T>việc liệt kê nội dung bộ sưu tập tồn tại khi nó được tạo ra là hoàn toàn chấp nhận được . ngoại lệ được sửa đổi bộ sưu tập chỉ được yêu cầu nếu điều tra viên không thể đảm bảo hoạt động không có trục trặc.
supercat

Lặp lại thông qua bộ sưu tập MT, cách thiết kế có thể dẫn đến một ngoại lệ ... Cái nào tôi không biết. Bạn sẽ bẫy tất cả các ngoại lệ? Trong cuốn sách của tôi, ngoại lệ là ngoại lệ và không nên xảy ra khi thực thi mã thông thường. Mặt khác, để ngăn chặn ngoại lệ, bạn phải khóa bộ sưu tập hoặc lấy một bản sao (theo cách an toàn - tức là khóa) hoặc thực hiện cơ chế rất phức tạp trong bộ sưu tập để ngăn chặn ngoại lệ xảy ra do đồng thời. Mặc dù vậy, tôi thấy thật tuyệt khi thêm IEnumeratorMT sẽ khóa bộ sưu tập trong khi mỗi lần xảy ra và thêm mã liên quan ...
Eric Ouellet

Một điều khác cũng có thể xảy ra là khi bạn nhận được một trình vòng lặp, bạn có thể khóa bộ sưu tập và khi trình vòng lặp của bạn được thu thập, bạn có thể mở khóa bộ sưu tập. Theo microsfot, họ đã kiểm tra xem IEnumerable có phải là IDis Dùng hay không và gọi cho GC nếu có ở cuối ForEach. Vấn đề chính là họ cũng sử dụng IEnumerable ở nơi khác mà không gọi cho GC, sau đó bạn không thể dựa vào đó. Có một giao diện MT rõ ràng mới cho IEnumerable cho phép khóa sẽ giải quyết được vấn đề, ít nhất là một phần của nó. (Nó sẽ không ngăn cản mọi người không gọi nó).
Eric Ouellet

Đó là một hình thức rất xấu cho một GetEnumeratorphương thức công khai để lại một bộ sưu tập bị khóa sau khi nó trở lại; thiết kế như vậy có thể dễ dàng dẫn đến bế tắc. Việc IEnumerable<T>cung cấp không có dấu hiệu nào cho thấy việc liệt kê có thể được hoàn thành ngay cả khi một bộ sưu tập được sửa đổi hay không; cách tốt nhất có thể làm là viết các phương thức của riêng mình để họ sẽ làm như vậy và có các phương thức chấp nhận IEnumerable<T>tài liệu thực tế sẽ chỉ an toàn cho IEnumerable<T>luồng nếu hỗ trợ liệt kê an toàn luồng.
supercat

Điều gì sẽ hữu ích nhất sẽ có nếu IEnumerable<T>bao gồm phương pháp "Ảnh chụp nhanh" với kiểu trả về IEnumerable<T>. Bộ sưu tập bất biến có thể trở lại chính họ; một bộ sưu tập giới hạn có thể nếu không có gì khác sao chép chính nó vào một List<T>hoặc T[]và gọi GetEnumeratornó. Một số bộ sưu tập không giới hạn có thể thực hiện Snapshotvà những bộ sưu tập không thể đưa ra một ngoại lệ mà không cố gắng điền vào danh sách với nội dung của chúng.
supercat

1

Trong mã thực thi tuần tự, các cấu trúc dữ liệu được sử dụng khác với mã thực thi đồng thời (được viết tốt). Lý do là mã tuần tự ngụ ý thứ tự ngầm. Mã đồng thời tuy nhiên không ngụ ý bất kỳ thứ tự nào; tốt hơn nữa nó ngụ ý việc thiếu bất kỳ trật tự được xác định!

Do đó, các cấu trúc dữ liệu với thứ tự ngụ ý (như Danh sách) không hữu ích cho việc giải quyết các vấn đề đồng thời. Một danh sách ngụ ý thứ tự, nhưng nó không xác định rõ thứ tự đó là gì. Do đó, thứ tự thực thi của mã thao tác danh sách sẽ xác định (ở một mức độ nào đó) thứ tự ngầm của danh sách, xung đột trực tiếp với một giải pháp đồng thời hiệu quả.

Hãy nhớ đồng thời là một vấn đề dữ liệu, không phải là một vấn đề mã! Bạn không thể triển khai mã trước (hoặc viết lại mã tuần tự hiện có) và có được một giải pháp đồng thời được thiết kế tốt. Bạn cần thiết kế cấu trúc dữ liệu trước trong khi lưu ý rằng thứ tự ngầm không tồn tại trong một hệ thống đồng thời.


1

Phương pháp sao chép và ghi không khóa hoạt động tốt nếu bạn không xử lý quá nhiều mục. Đây là một lớp tôi đã viết:

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

sử dụng ví dụ: order_BUY = CopyAndWriteList.Clear (order_BUY);


thay vì khóa, nó tạo một bản sao của danh sách, sửa đổi danh sách và đặt tham chiếu vào danh sách mới. Vì vậy, bất kỳ chủ đề khác đang lặp đi lặp lại sẽ không gây ra bất kỳ vấn đề.
Cướp số lượng

0

Tôi đã thực hiện một cái tương tự như của Brian . Của tôi là khác nhau:

  • Tôi quản lý mảng trực tiếp.
  • Tôi không nhập các ổ khóa trong khối thử.
  • Tôi sử dụng yield returnđể sản xuất một điều tra viên.
  • Tôi hỗ trợ đệ quy khóa. Điều này cho phép đọc từ danh sách trong quá trình lặp.
  • Tôi sử dụng khóa đọc nâng cấp nếu có thể.
  • DoSyncGetSynccác phương thức cho phép các tương tác tuần tự yêu cầu quyền truy cập độc quyền vào danh sách.

:

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}

Điều gì xảy ra nếu hai luồng vào đầu trykhối trong Removehoặc bộ chỉ mục được thiết lập cùng một lúc?
James

@James dường như không thể. Đọc nhận xét tại msdn.microsoft.com/en-us/l Library / . Chạy mã này, bạn sẽ không bao giờ nhập khóa đó lần thứ 2: gist.github.com/ronnieoverby/59b715c3676127a113c3
Ronnie Overby

@Ronny Overby: Thú vị. Do đó, tôi nghi ngờ rằng điều này sẽ hoạt động tốt hơn nhiều nếu bạn gỡ bỏ UpgradableReadLock khỏi tất cả các chức năng trong đó thao tác duy nhất được thực hiện trong thời gian giữa khóa đọc nâng cấp và khóa ghi - chi phí sử dụng bất kỳ loại khóa nào là nhiều hơn hơn kiểm tra để xem nếu tham số nằm ngoài phạm vi mà chỉ cần thực hiện kiểm tra bên trong khóa ghi có thể sẽ hoạt động tốt hơn.
James

Lớp này cũng có vẻ không hữu ích lắm, vì các hàm dựa trên offset (hầu hết trong số chúng) thực sự không bao giờ được sử dụng một cách an toàn trừ khi có một số lược đồ khóa bên ngoài vì bộ sưu tập có thể thay đổi giữa khi bạn quyết định đặt hoặc đặt ở đâu nhận được một cái gì đó từ và khi bạn thực sự có được nó.
James

1
Tôi muốn ghi lại rằng tôi nhận ra rằng tính hữu ích của IListngữ nghĩa trong các tình huống đồng thời bị hạn chế tối đa. Tôi đã viết mã này có lẽ trước khi tôi nhận ra điều đó. Kinh nghiệm của tôi cũng giống như người viết câu trả lời được chấp nhận: Tôi đã thử nó với những gì tôi biết về đồng bộ hóa và IList <T> và tôi đã học được điều gì đó bằng cách làm điều đó.
Ronnie Overby
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 cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.