.NET - Khóa từ điển so với đồng thời


131

Tôi không thể tìm thấy đủ thông tin về ConcurrentDictionarycác loại, vì vậy tôi nghĩ tôi sẽ hỏi về nó ở đây.

Hiện tại, tôi sử dụng a Dictionaryđể giữ tất cả người dùng được truy cập liên tục bởi nhiều luồng (từ nhóm luồng, do đó không có số lượng luồng chính xác) và nó có quyền truy cập được đồng bộ hóa.

Gần đây tôi phát hiện ra rằng có một bộ các bộ sưu tập an toàn luồng trong .NET 4.0, và nó có vẻ rất dễ chịu. Tôi đã tự hỏi, điều gì sẽ là tùy chọn 'hiệu quả hơn và dễ quản lý hơn', vì tôi có tùy chọn giữa việc có một Dictionarytruy cập bình thường với quyền truy cập được đồng bộ hóa hoặc có một sự ConcurrentDictionaryan toàn theo luồng.

Tham chiếu đến .NET 4.0 ConcurrentDictionary


Câu trả lời:


146

Một bộ sưu tập an toàn chủ đề so với một bộ sưu tập không an toàn chủ đề có thể được xem xét theo một cách khác.

Hãy xem xét một cửa hàng không có nhân viên bán hàng, ngoại trừ lúc thanh toán. Bạn có vô số vấn đề nếu mọi người không hành động có trách nhiệm. Chẳng hạn, giả sử một khách hàng lấy một lon từ kim tự tháp trong khi một nhân viên bán hàng hiện đang xây dựng kim tự tháp, tất cả địa ngục sẽ vỡ ra. Hoặc, nếu hai khách hàng tiếp cận cùng một mặt hàng cùng một lúc, ai sẽ thắng? Sẽ có một cuộc chiến? Đây là một bộ sưu tập không chủ đề. Có rất nhiều cách để tránh các vấn đề, nhưng tất cả chúng đều yêu cầu một số loại khóa, hoặc truy cập rõ ràng hơn bằng cách này hay cách khác.

Mặt khác, hãy xem xét một cửa hàng với một nhân viên bán hàng tại bàn và bạn chỉ có thể mua sắm thông qua anh ta. Bạn xếp hàng, và yêu cầu anh ta cho một món đồ, anh ta mang nó lại cho bạn, và bạn đi ra khỏi hàng. Nếu bạn cần nhiều mặt hàng, bạn chỉ có thể nhặt được bao nhiêu mặt hàng trên mỗi vòng mà bạn có thể nhớ, nhưng bạn cần cẩn thận để tránh làm phiền nhân viên bán hàng, điều này sẽ khiến các khách hàng khác đứng sau bạn.

Bây giờ hãy xem xét điều này. Trong cửa hàng có một nhân viên bán hàng, điều gì sẽ xảy ra nếu bạn đi hết hàng trước và hỏi nhân viên bán hàng "Bạn có giấy vệ sinh nào không" và anh ta nói "Có", rồi bạn nói "Ok, tôi ' sẽ quay lại với bạn khi tôi biết tôi cần bao nhiêu ", sau đó khi bạn quay lại đầu hàng, cửa hàng tất nhiên có thể được bán hết. Kịch bản này không được ngăn chặn bởi một bộ sưu tập chủ đề.

Một bộ sưu tập chủ đề đảm bảo rằng cấu trúc dữ liệu bên trong của nó luôn hợp lệ, ngay cả khi được truy cập từ nhiều luồng.

Một bộ sưu tập không an toàn không đi kèm với bất kỳ đảm bảo như vậy. Chẳng hạn, nếu bạn thêm một cái gì đó vào cây nhị phân trên một luồng, trong khi một luồng khác đang bận cân bằng lại cây, thì không có gì đảm bảo vật phẩm sẽ được thêm vào, hoặc thậm chí cây vẫn còn hiệu lực sau đó, nó có thể bị hỏng ngoài hy vọng.

Tuy nhiên, một bộ sưu tập luồng không đảm bảo rằng các hoạt động tuần tự trên luồng đều hoạt động trên cùng một "ảnh chụp nhanh" của cấu trúc dữ liệu bên trong của nó, điều đó có nghĩa là nếu bạn có mã như thế này:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

bạn có thể nhận được một NullReferenceException vì giữa tree.Counttree.First(), một luồng khác đã xóa các nút còn lại trong cây, có nghĩa là First()sẽ trả về null.

Đối với kịch bản này, bạn cần phải xem liệu bộ sưu tập được đề cập có cách an toàn để có được những gì bạn muốn hay không, có lẽ bạn cần phải viết lại mã ở trên, hoặc bạn có thể cần phải khóa.


9
Mỗi lần tôi đọc bài viết này, mặc dù tất cả đều đúng, nhưng bằng cách nào đó chúng ta đang đi sai đường.
scope_creep

19
Vấn đề chính trong một ứng dụng kích hoạt luồng là các cấu trúc dữ liệu có thể thay đổi. Nếu bạn có thể tránh điều đó, bạn sẽ tự cứu mình vô số rắc rối.
Lasse V. Karlsen

89
Đây là một lời giải thích hay về an toàn luồng, tuy nhiên tôi không thể không cảm thấy điều này thực sự không giải quyết được câu hỏi của OP. Những gì OP đã hỏi (và những gì sau đó tôi đã gặp phải câu hỏi này) là sự khác biệt giữa việc sử dụng một tiêu chuẩn Dictionaryvà tự xử lý khóa so với sử dụng ConcurrentDictionaryloại được tích hợp vào .NET 4+. Tôi thực sự có một chút trở ngại rằng điều này đã được chấp nhận.
mclark1129

4
An toàn chủ đề không chỉ là sử dụng đúng bộ sưu tập. Sử dụng bộ sưu tập phù hợp là một sự khởi đầu, không phải là điều duy nhất bạn phải đối phó. Tôi đoán đó là những gì OP muốn biết. Tôi không thể đoán tại sao điều này được chấp nhận, đã được một thời gian kể từ khi tôi viết nó.
Lasse V. Karlsen

2
Tôi nghĩ điều mà @ LasseV.Karlsen đang cố nói, đó là ... an toàn luồng rất dễ đạt được, nhưng bạn phải cung cấp một mức độ đồng thời trong cách bạn xử lý sự an toàn đó. Hãy tự hỏi mình điều này, "Tôi có thể thực hiện nhiều thao tác cùng một lúc trong khi giữ cho đối tượng an toàn không?" Xem xét ví dụ này: Hai khách hàng muốn trả lại các mặt hàng khác nhau. Khi bạn, với tư cách là người quản lý, thực hiện chuyến đi để đặt lại cửa hàng của bạn, bạn có thể không chỉ giới hạn cả hai mặt hàng trong một chuyến đi mà vẫn xử lý cả hai yêu cầu của khách hàng hay bạn sẽ thực hiện một chuyến đi mỗi khi khách hàng trả lại một mặt hàng?
Alexandru

68

Bạn vẫn cần phải rất cẩn thận khi sử dụng các bộ sưu tập an toàn luồng vì an toàn luồng không có nghĩa là bạn có thể bỏ qua tất cả các vấn đề luồng. Khi một bộ sưu tập tự quảng cáo là an toàn cho chuỗi, điều đó thường có nghĩa là nó vẫn ở trạng thái nhất quán ngay cả khi nhiều luồng đang đọc và viết đồng thời. Nhưng điều đó không có nghĩa là một luồng sẽ thấy một chuỗi kết quả "hợp lý" nếu nó gọi nhiều phương thức.

Ví dụ: nếu trước tiên bạn kiểm tra xem một khóa có tồn tại không và sau đó nhận được giá trị tương ứng với khóa đó, khóa đó có thể không còn tồn tại ngay cả với phiên bản ConcurrencyDipedia (vì một luồng khác có thể đã xóa khóa). Bạn vẫn cần sử dụng khóa trong trường hợp này (hoặc tốt hơn: kết hợp hai cuộc gọi bằng cách sử dụng TryGetValue ).

Vì vậy, hãy sử dụng chúng, nhưng đừng nghĩ rằng nó mang lại cho bạn một lượt miễn phí để bỏ qua tất cả các vấn đề tương tranh. Bạn vẫn cần phải cẩn thận.


4
Vì vậy, về cơ bản bạn đang nói nếu bạn không sử dụng đúng đối tượng thì nó sẽ không hoạt động?
ChaosPandion

3
Về cơ bản, bạn cần sử dụng TryAdd thay vì các Chứa thông thường -> Thêm.
ChaosPandion

3
@Bruno: Không. Nếu bạn chưa khóa, một chuỗi khác có thể đã xóa khóa giữa hai cuộc gọi.
Mark Byers

13
Không có nghĩa là âm thanh cộc lốc ở đây, nhưng TryGetValueluôn luôn là một phần Dictionaryvà luôn luôn là phương pháp được đề xuất. Các phương pháp "đồng thời" quan trọng ConcurrentDictionarygiới thiệu là AddOrUpdateGetOrAdd. Vì vậy, câu trả lời tốt, nhưng có thể đã chọn một ví dụ tốt hơn.
Aaronaught

4
Phải - không giống như trong cơ sở dữ liệu có giao dịch, hầu hết cấu trúc tìm kiếm trong bộ nhớ đồng thời bỏ lỡ khái niệm cô lập , chúng chỉ đảm bảo cấu trúc dữ liệu bên trong là chính xác.
Chang

39

Nội bộ đồng thời Từ điển sử dụng một khóa riêng cho mỗi nhóm băm. Miễn là bạn chỉ sử dụng Thêm / TryGetValue và các phương thức tương tự hoạt động trên các mục đơn, từ điển sẽ hoạt động như một cấu trúc dữ liệu gần như không khóa với lợi ích hiệu suất ngọt tương ứng. OTOH các phương thức liệt kê (bao gồm cả thuộc tính Count) khóa tất cả các nhóm cùng một lúc và do đó kém hơn so với Từ điển được đồng bộ hóa, hiệu suất khôn ngoan.

Tôi muốn nói, chỉ cần sử dụng đồng thời.


15

Tôi nghĩ rằng phương thức ConcurrencyDipedia.GetOrAdd chính xác là những gì hầu hết các kịch bản đa luồng cần.


1
Tôi cũng sẽ sử dụng TryGet, nếu không, bạn sẽ lãng phí công sức trong việc khởi tạo tham số thứ hai thành GetOrAdd mỗi khi khóa tồn tại.
Jorrit Schippers

7
GetOrAdd có quá tải GetOrAdd (TKey, Func <TKey, TValue>) , vì vậy tham số thứ hai có thể được khởi tạo lười biếng chỉ trong trường hợp khóa không tồn tại trong từ điển. Bên cạnh đó, TryGetValue được sử dụng để tìm nạp dữ liệu, không sửa đổi. Các cuộc gọi tiếp theo đến từng phương thức này có thể khiến một luồng khác có thể đã thêm hoặc xóa khóa giữa hai cuộc gọi.
sgnsajgon

Đây phải là câu trả lời được đánh dấu cho câu hỏi, mặc dù bài của Lasse rất xuất sắc.
Spivonious

13

Bạn đã thấy phần mở rộng Reactive cho .Net 3.5sp1. Theo Jon Skeet, họ đã đưa ra một gói các phần mở rộng song song và cấu trúc dữ liệu đồng thời cho .Net3.5 sp1.

Có một bộ mẫu cho .Net 4 Beta 2, mô tả rất chi tiết về cách sử dụng chúng các phần mở rộng song song.

Tôi vừa trải qua tuần trước để thử nghiệm Đồng thời Từ điển bằng cách sử dụng 32 luồng để thực hiện I / O. Nó dường như hoạt động như quảng cáo, điều này cho thấy một lượng lớn thử nghiệm đã được đưa vào nó.

Biên tập : .NET 4 Đồng thời Từ điển và mẫu.

Microsoft đã phát hành một bản pdf có tên là Các mẫu lập trình Paralell. Nó thực sự đáng để tải xuống vì nó được mô tả rất chi tiết về các mẫu phù hợp để sử dụng cho .Net 4 Các tiện ích mở rộng đồng thời và các mẫu chống cần tránh. Nó đây rồi


4

Về cơ bản, bạn muốn sử dụng đồng thời mới. Ngay lập tức bạn phải viết ít mã hơn để tạo các chương trình an toàn cho chuỗi.


Có vấn đề gì nếu tôi tiếp tục với Từ điển đồng thời?
kbvishnu

1

Các ConcurrentDictionary là một lựa chọn tuyệt vời nếu nó đáp ứng tất cả các nhu cầu an toàn luồng của bạn. Nếu không, nói cách khác, bạn đang làm bất cứ điều gì hơi phức tạp, bình thường Dictionary+ lockcó thể là một lựa chọn tốt hơn. Ví dụ: giả sử bạn đang thêm một số đơn hàng vào từ điển và bạn muốn tiếp tục cập nhật tổng số lượng đơn hàng. Bạn có thể viết mã như thế này:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Mã này không phải là chủ đề an toàn. Nhiều luồng cập nhật _totalAmounttrường có thể khiến nó ở trạng thái bị hỏng. Vì vậy, bạn có thể cố gắng bảo vệ nó bằng lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Mã này "an toàn hơn", nhưng vẫn không an toàn cho chuỗi. Không có gì đảm bảo rằng _totalAmountnó phù hợp với các mục trong từ điển. Một luồng khác có thể cố đọc các giá trị này, để cập nhật phần tử UI:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

Các totalAmount thể không tương ứng với số lượng đơn đặt hàng trong từ điển. Các số liệu thống kê hiển thị có thể sai. Tại thời điểm này, bạn sẽ nhận ra rằng bạn phải mở rộng lockbảo vệ để bao gồm việc cập nhật từ điển:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Mã này là hoàn toàn an toàn, nhưng tất cả những lợi ích của việc sử dụng a ConcurrentDictionaryđã biến mất.

  1. Hiệu suất đã trở nên tồi tệ hơn so với sử dụng bình thường Dictionary , bởi vì khóa bên trongConcurrentDictionary hiện đang lãng phí và dư thừa.
  2. Bạn phải xem lại tất cả mã của mình để sử dụng các biến được chia sẻ không được bảo vệ.
  3. Bạn đang bị mắc kẹt với việc sử dụng API vụng về ( TryAdd?,AddOrUpdate ?).

Vì vậy, lời khuyên của tôi là: bắt đầu bằng dấu Dictionary+ lockvà giữ tùy chọn nâng cấp sau này thành ConcurrentDictionarytối ưu hóa hiệu suất, nếu tùy chọn này thực sự khả thi. Trong nhiều trường hợp nó sẽ không được.


0

Chúng tôi đã sử dụng ConcurrencyDixi cho bộ sưu tập được lưu trong bộ nhớ cache, được điền lại sau mỗi 1 giờ và sau đó được đọc bởi nhiều luồng khách, tương tự như giải pháp cho luồng ví dụ này có an toàn không? câu hỏi

Chúng tôi thấy rằng việc thay đổi nó thành  ReadOnlyDipedia đã  cải thiện hiệu suất tổng thể.


ReadOnlyDipedia chỉ là trình bao bọc xung quanh một IDadata khác. Bạn sử dụng gì dưới mui xe?
Lu55

@ Lu55, chúng tôi đã sử dụng thư viện MS .Net và chọn trong số các từ điển có sẵn / áp dụng. Tôi không biết về việc triển khai nội bộ MS của ReadOnlyDixi
Michael Freidgeim
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.