2017-12-04 71 views
9

Giả định làm việc của tôi là LINQ là an toàn luồng khi được sử dụng với các bộ sưu tập System.Collections.Concurrent (bao gồm ConcurrentDictionary).Có phải C# LINQ OrderBy threads an toàn khi được sử dụng với ConcurrentDictionary <Tkey, TValue>?

(bài viết Overflow khác cũng đồng ý với link)

Tuy nhiên, một thanh tra việc thực hiện của LINQ OrderBy phương pháp khuyến nông cho thấy rằng nó không xuất hiện để được thread với các tập hợp con của các bộ sưu tập đồng thời mà thực hiện ICollection (ví dụ: ConcurrentDictionary).

Các OrderedEnumerableGetEnumerator (source here) xây dựng một thể hiện của một Buffer struct (source here) mà cố gắng để đúc các bộ sưu tập để một ICollection (mà ConcurrentDictionary dụng cụ) và sau đó thực hiện một bộ sưu tập. CopyTo với một mảng được khởi tạo với kích thước của bộ sưu tập.

Do đó, nếu ConcurrentDictionary (như bê tông ICollection trong trường hợp này) phát triển về kích thước trong khi phẫu thuật OrderBy, giữa initialising mảng và sao chép vào nó, hoạt động này sẽ ném.

Các mã kiểm tra dưới đây cho thấy ngoại lệ này:

(Lưu ý: Tôi đánh giá cao mà thực hiện một OrderBy trên một bộ sưu tập thread-safe đó đang thay đổi bên dưới bạn mà không phải là có ý nghĩa, nhưng tôi không tin điều đó nên ném)

using System; 
using System.Collections.Concurrent; 
using System.Linq; 
using System.Threading; 
using System.Threading.Tasks; 

namespace Program 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      try 
      { 
       int loop = 0; 
       while (true) //Run many loops until exception thrown 
       { 
        Console.WriteLine($"Loop: {++loop}"); 

        _DoConcurrentDictionaryWork().Wait(); 
       } 
      } 
      catch (Exception ex) 
      { 
       Console.WriteLine(ex); 
      } 
     } 

     private static async Task _DoConcurrentDictionaryWork() 
     { 
      var concurrentDictionary = new ConcurrentDictionary<int, object>(); 
      var keyGenerator = new Random(); 
      var tokenSource = new CancellationTokenSource(); 

      var orderByTaskLoop = Task.Run(() => 
      { 
       var token = tokenSource.Token; 
       while (token.IsCancellationRequested == false) 
       { 
        //Keep ordering concurrent dictionary on a loop 
        var orderedPairs = concurrentDictionary.OrderBy(x => x.Key).ToArray(); //THROWS EXCEPTION HERE 

        //...do some more work with ordered snapshot... 
       } 
      }); 

      var updateDictTaskLoop = Task.Run(() => 
      { 
       var token = tokenSource.Token; 
       while (token.IsCancellationRequested == false) 
       { 
        //keep mutating dictionary on a loop 
        var key = keyGenerator.Next(0, 1000); 
        concurrentDictionary[key] = new object(); 
       } 
      }); 

      //Wait for 1 second 
      await Task.Delay(TimeSpan.FromSeconds(1)); 

      //Cancel and dispose token 
      tokenSource.Cancel(); 
      tokenSource.Dispose(); 

      //Wait for orderBy and update loops to finish (now token cancelled) 
      await Task.WhenAll(orderByTaskLoop, updateDictTaskLoop); 
     } 
    } 
} 

Đó là OrderBy ném một ngoại lệ dẫn đến một trong số ít những kết luận có thể:

1) Giả định của tôi về LINQ là luồng an toàn với các bộ sưu tập đồng thời là không chính xác, và nó chỉ an toàn để thực hiện LINQ trên các bộ sưu tập (chúng đồng thời hay không) mà không bị đột biến trong truy vấn LINQ

2) Có lỗi với việc triển khai LINQ OrderBy và việc triển khai thử nghiệm và truyền tập hợp nguồn đến ICollection là không chính xác và thử và thực hiện sao chép bộ sưu tập (và nó chỉ cần chuyển sang hành vi mặc định của nó lặp lại IEnumerable).

3) Tôi đã hiểu lầm những gì đang xảy ra ở đây ...

Suy nghĩ được đánh giá cao!

+3

_ "NGOẠI TRỪ NÀY" _ Ngoại lệ là gì? –

+1

Giả định của bạn rằng hàm tạo bộ đệm tạo thành 'ICollection ' sẽ thành công là sai. Bởi vì ['ConcurrentDictionary.GetEnumerator'] (https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,b2dcb93f9ede4ba0,references) là nguồn thực tế và đây không phải là một' ICollection '. –

+0

Có vẻ như bạn đã trả lời câu hỏi của riêng mình, phải không? Ý tôi là bạn thấy mình không an toàn để làm điều này. – Evk

Trả lời

5

Nó không được nêu ở bất kỳ đâu OrderBy (hoặc các phương pháp LINQ khác) phải luôn sử dụng GetEnumerator của nguồn IEnumerable hoặc nó phải là luồng an toàn trên bộ sưu tập đồng thời. Tất cả những gì được hứa hẹn là phương pháp này

Sắp xếp các phần tử của chuỗi theo thứ tự tăng dần theo khóa .

ConcurrentDictionary không phải là chủ đề an toàn theo nghĩa toàn cầu. Đó là chủ đề an toàn đối với các hoạt động khác được thực hiện trên đó. Thậm chí nhiều hơn, tài liệu nói rằng

Tất cả các thành viên công cộng và được bảo vệ của ConcurrentDictionary đều an toàn và có thể được sử dụng đồng thời từ nhiều chủ đề. Tuy nhiên, các thành viên truy cập thông qua một trong những giao diện các cụ ConcurrentDictionary, bao gồm cả phần mở rộng phương pháp, không đảm bảo được đề an toàn và có thể cần phải đồng bộ bởi người gọi.

Vì vậy, sự hiểu biết của bạn là chính xác (OrderBy sẽ thấy IEnumerable bạn vượt qua để nó thực sự là ICollection, sau đó sẽ có được chiều dài của bộ sưu tập đó, phân bổ đệm kích thước đó, sau đó sẽ gọi ICollection.CopyTo, và điều này là tất nhiên không chỉ an toàn trên bất kỳ loại bộ sưu tập nào), nhưng nó không phải là lỗi trong OrderBy vì không phải OrderBy cũng không phải ConcurrentDictionary từng hứa với những gì bạn giả định.

Nếu bạn muốn làm OrderBy trong một chủ đề an toàn cách trên ConcurrentDictionary, bạn cần phải dựa vào các phương pháp được hứa là chuỗi an toàn. Ví dụ:

// note: this is NOT IEnumerable.ToArray() 
// but public ToArray() method of ConcurrentDictionary itself 
// it is guaranteed to be thread safe with respect to other operations 
// on this dictionary 
var snapshot = concurrentDictionary.ToArray(); 
// we are working on snapshot so no one other thread can modify it 
// of course at this point real contents of dictionary might not be 
// the same as our snapshot 
var sorted = snapshot.OrderBy(c => c.Key); 

Nếu bạn không muốn phân bổ mảng bổ sung (với ToArray), bạn có thể sử dụng Select(c => c) và nó sẽ làm việc trong trường hợp này, nhưng sau đó chúng tôi một lần nữa trong lãnh thổ tranh luận và dựa vào cái gì để được an toàn để sử dụng trong trường hợp nó không được hứa hẹn (Select cũng sẽ không luôn luôn liệt kê bộ sưu tập của bạn. Nếu bộ sưu tập là mảng hoặc danh sách - nó sẽ tắt và sử dụng các chỉ mục thay thế). Vì vậy, bạn có thể tạo ra phương pháp khuyến nông như thế này:

public static class Extensions { 
    public static IEnumerable<T> ForceEnumerate<T>(this ICollection<T> collection) { 
     foreach (var item in collection) 
      yield return item; 
    } 
} 

Và sử dụng nó như thế này nếu bạn muốn được an toàn và không muốn phân bổ mảng:

concurrentDictionary.ForceEnumerate().OrderBy(c => c.Key).ToArray(); 

Trong trường hợp này chúng ta đang buộc liệt kê của ConcurrentDictionary (mà chúng tôi biết là an toàn từ tài liệu) và sau đó vượt qua đó để OrderBy biết rằng nó không thể làm bất kỳ tác hại với tinh khiết IEnumerable. Lưu ý rằng như được chỉ ra một cách chính xác trong các chú thích bởi mjwills, điều này không chính xác giống như ToArray, bởi vì ToArray tạo ảnh chụp nhanh (bộ sưu tập khóa ngăn ngừa các sửa đổi trong khi xây dựng mảng) và Select \ yield không có bất kỳ khóa nào (vì vậy các mục có thể được thêm vào \ remove ngay khi điều tra đang được tiến hành). Mặc dù tôi nghi ngờ nó quan trọng khi làm những việc như mô tả trong câu hỏi - trong cả hai trường hợp sau khi hoàn thành OrderBy - bạn không biết liệu kết quả của bạn có phản ánh trạng thái hiện tại của bộ sưu tập hay không.

+0

Có thể 'var snapshot = concurrentDictionary.Select (z => z);' thay vào đó được sử dụng để tránh phân bổ mảng? – mjwills

+1

@mjwills Tôi đã cập nhật câu trả lời với những suy nghĩ của tôi về điều đó. – Evk

+0

Công việc tuyệt vời @Evk. Việc sử dụng 'ToArray' hoặc' Select'/'yield' có phụ thuộc phần lớn vào việc bạn muốn có ảnh chụp nhanh (tức là điểm trong thời gian) hay không. https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ 'ToArray' sẽ cung cấp cho bạn ngữ nghĩa chụp nhanh, nhưng các khóa liên quan có nghĩa là nó thường sẽ hơi chậm hơn (chưa kể đến bộ nhớ Giá cả). – mjwills

Các vấn đề liên quan