.NET - Sözlük kilitleme ve ConcurrentDictionary


131

Türler hakkında yeterince bilgi bulamadım ConcurrentDictionary, bu yüzden burada sormayı düşündüm.

Şu anda, Dictionarybirden fazla iş parçacığı tarafından (bir iş parçacığı havuzundan, dolayısıyla tam olarak iş parçacığı miktarı yok) sürekli erişilen tüm kullanıcıları tutmak için a kullanıyorum ve eşitlenmiş erişime sahip.

Kısa bir süre önce .NET 4.0'da bir dizi iş parçacığı güvenli koleksiyon olduğunu öğrendim ve bu çok hoş görünüyor. DictionarySenkronize erişime sahip normal veya ConcurrentDictionaryzaten iş parçacığı için güvenli olan bir seçeneğe sahip olduğum için 'daha verimli ve yönetmesi daha kolay' seçeneğinin ne olacağını merak ediyordum .

.NET 4.0'lara referans ConcurrentDictionary


Yanıtlar:


146

İş parçacığı açısından güvenli bir koleksiyona karşı iş parçacığı olmayan bir koleksiyon farklı bir şekilde incelenebilir.

Kasa dışında, katip olmayan bir mağaza düşünün. İnsanlar sorumlu davranmazlarsa bir sürü sorununuz var. Örneğin, bir müşteri şu anda piramidi inşa ederken bir piramit kutusundan bir kutu aldığını varsayalım, tüm cehennem kırılır. Ya da iki müşteri aynı ürüne aynı anda ulaşırsa kim kazanır? Bir kavga olacak mı? Bu, iş parçacığı olmayan bir koleksiyondur. Sorunlardan kaçınmanın birçok yolu var, ancak hepsi bir tür kilitleme veya bir şekilde açık erişim gerektirir.

Öte yandan, masasında bir katip olan bir mağaza düşünün ve sadece onun aracılığıyla alışveriş yapabilirsiniz. Sıraya girin ve ondan bir eşya isteyin, size geri getirir ve çizginin dışına çıkarsınız. Birden fazla öğeye ihtiyacınız varsa, her gidiş-dönüş yolculuğunda hatırlayabildiğiniz kadar öğe alabilirsiniz, ancak katibi zorlamaktan kaçınmak için dikkatli olmanız gerekir, bu, arkanızdaki diğer müşterileri kızdırır.

Şimdi bunu bir düşünün. Bir memurun olduğu dükkanda, sıranın sonuna kadar gidip memura "Hiç tuvalet kağıdın var mı?" Diye sorarsan ve o "Evet" derse ve sonra "Tamam, ben" dersen Ne kadar ihtiyacım olduğunu bildiğimde size geri döneceğim ", sonra sıranın önüne geri döndüğünüzde, mağaza elbette tükenebilir. Bu senaryo, iş parçacığı güvenli bir koleksiyon tarafından engellenmez.

İş parçacığı güvenli bir koleksiyon, birden çok iş parçacığından erişilse bile, dahili veri yapılarının her zaman geçerli olduğunu garanti eder.

İş parçacığı güvenli olmayan bir koleksiyon, bu tür garantilerle birlikte gelmez. Örneğin, bir iş parçacığındaki bir ikili ağaca bir şey eklerseniz, başka bir iş parçacığı ağacı yeniden dengelemekle meşgulse, öğenin ekleneceğinin garantisi yoktur veya ağacın daha sonra hala geçerli olacağının garantisi yoktur, umudun ötesinde bozulmuş olabilir.

Ancak, iş parçacığı güvenli bir koleksiyon, iş parçacığı üzerindeki ardışık işlemlerin hepsinin dahili veri yapısının aynı "anlık görüntüsü" üzerinde çalışacağını garanti etmez; bu, bunun gibi bir kodunuz varsa:

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

bir NullReferenceException alabilirsiniz çünkü inbetween tree.Countve tree.First(), başka bir iş parçacığı ağaçtaki kalan düğümleri temizledi, bu da First()geri döneceği anlamına gelir null.

Bu senaryo için, söz konusu koleksiyonun istediğinizi elde etmek için güvenli bir yolu olup olmadığını görmeniz gerekir, belki yukarıdaki kodu yeniden yazmanız veya kilitlemeniz gerekebilir.


9
Bu makaleyi her okuduğumda, hepsi doğru olsa da, bir şekilde yanlış yola giriyoruz.
kapsam_creep

19
İş parçacığı etkin bir uygulamadaki ana sorun, değiştirilebilir veri yapılarıdır. Bundan kaçınabilirseniz, kendinizi bir sürü beladan kurtarırsınız.
Lasse V.Karlsen

89
Bu, iş parçacığı güvenliğinin güzel bir açıklaması, ancak yardım edemem ama bunun aslında OP'nin sorusuna hitap etmediğini hissediyorum. OP'nin sorduğu (ve daha sonra bu soruyla karşılaştığım şey), bir standart Dictionarykullanmakla kendiniz kilitlemeyi ele ConcurrentDictionaryalmakla .NET 4 + 'da yerleşik olan türü kullanmak arasındaki farktı. Aslında bunun kabul edilmesine biraz şaşırdım.
mclark1129

4
İş parçacığı güvenliği, doğru koleksiyonu kullanmaktan daha fazlasıdır. Doğru koleksiyonu kullanmak, uğraşmanız gereken tek şey değil, bir başlangıçtır. Sanırım OP'nin bilmek istediği buydu. Bunun neden kabul edildiğini tahmin edemiyorum, bunu yazmayalı epey oldu.
Lasse V.Karlsen

2
Sanırım @ LasseV.Karlsen'in demeye çalıştığı şu: iş parçacığı güvenliğini sağlamak kolaydır, ancak bu güvenliği nasıl idare edeceğiniz konusunda bir eşzamanlılık düzeyi sağlamalısınız. Kendinize şunu sorun: "Nesne iş parçacığını güvende tutarken aynı anda daha fazla işlem yapabilir miyim?" Şu örneği düşünün: İki müşteri farklı ürünleri iade etmek istiyor. Yönetici olarak, mağazanızın stoklarını yeniden doldurmak için bir yolculuk yaptığınızda, bir seferde her iki ürünü de yeniden stoklayıp yine de her iki müşterinin isteklerini yerine getiremez misiniz, yoksa bir müşteri her ürün iade ettiğinde bir yolculuk mu yapacaksınız?
Alexandru

68

İş parçacığı açısından güvenli koleksiyonları kullanırken yine de çok dikkatli olmanız gerekir çünkü iş parçacığı güvenli, tüm iş parçacığı sorunlarını göz ardı edebileceğiniz anlamına gelmez. Bir koleksiyon kendini iş parçacığı için güvenli olarak tanıtırsa, bu genellikle birden çok iş parçacığı aynı anda okurken ve yazarken bile tutarlı bir durumda kaldığı anlamına gelir. Ancak bu, tek bir iş parçacığının birden çok yöntemi çağırması durumunda "mantıksal" bir sonuç dizisi göreceği anlamına gelmez.

Örneğin, önce bir anahtarın var olup olmadığını kontrol ederseniz ve daha sonra anahtara karşılık gelen değeri alırsanız, bu anahtar ConcurrentDictionary sürümünde bile artık mevcut olmayabilir (çünkü başka bir iş parçacığı anahtarı kaldırmış olabilir). Yine de bu durumda kilitlemeyi kullanmanız gerekir (veya daha iyisi: TryGetValue kullanarak iki çağrıyı birleştirin ).

Öyleyse onları kullanın, ancak tüm eşzamanlılık sorunlarını görmezden gelmek için size ücretsiz bir geçiş hakkı verdiğini düşünmeyin. Yine de dikkatli olmalısın.


4
Yani temelde nesneyi doğru kullanmazsanız işe yaramayacağını mı söylüyorsunuz?
ChaosPandion

3
Temel olarak, ortak İçerir -> Ekle yerine TryAdd kullanmanız gerekir.
ChaosPandion

3
@Bruno: Hayır. Kilitlemediyseniz, başka bir iş parçacığı iki arama arasındaki anahtarı kaldırmış olabilir.
Mark Byers

13
Burada kaba görünmek istemeyin, ancak TryGetValueher zaman Dictionaryönerilen yaklaşımın bir parçası olmuştur ve her zaman olmuştur. Önemli "eşzamanlı" yöntemleri olduğunu ConcurrentDictionarytanıtır vardır AddOrUpdateve GetOrAdd. Yani, iyi cevap, ama daha iyi bir örnek seçebilirdi.
Aaronaught

4
Doğru - işlem içeren veritabanından farklı olarak, çoğu eşzamanlı bellek içi arama yapısı izolasyon kavramını kaçırır , yalnızca dahili veri yapısının doğru olduğunu garanti eder.
Chang 08

39

Dahili olarak ConcurrentDictionary, her karma grubu için ayrı bir kilit kullanır. Yalnızca Add / TryGetValue ve tek girişler üzerinde çalışan benzer yöntemleri kullandığınız sürece, sözlük, ilgili tatlı performans avantajıyla neredeyse kilitsiz bir veri yapısı olarak çalışacaktır. OTOH, numaralandırma yöntemleri (Count özelliği dahil) tüm kovaları aynı anda kilitler ve bu nedenle, performans açısından senkronize bir Sözlükten daha kötüdür.

Sadece ConcurrentDictionary kullanın derdim.


15

ConcurrentDictionary.GetOrAdd yönteminin tam olarak çok iş parçacıklı senaryoların çoğunun ihtiyaç duyduğu şey olduğunu düşünüyorum.


1
TryGet'i de kullanırdım, aksi takdirde ikinci parametreyi GetOrAdd olarak başlatmak için çabayı boşa harcarsınız.
Jorrit Schippers

7
GetOrAdd , GetOrAdd (TKey, Func <TKey, TValue>) aşırı yüküne sahiptir , bu nedenle ikinci parametre yalnızca anahtarın sözlükte olmaması durumunda tembel olarak başlatılabilir. Ayrıca TryGetValue , verileri değiştirmek için değil, almak için kullanılır. Bu yöntemlerin her birine yapılan sonraki çağrılar, başka bir iş parçacığının iki çağrı arasına anahtarı eklemesine veya kaldırmasına neden olabilir.
sgnsajgon

Lasse'nin gönderisi mükemmel olsa da, sorunun cevabı bu olmalıdır.
Spivonious

13

Reaktif Uzantıları gördünüz mü.Net 3.5sp1 içinJon Skeet'e göre, .Net3.5 sp1 için paralel uzantıların ve eşzamanlı veri yapılarının bir demetini desteklediler.

.Net 4 Beta 2 için, paralel uzantıların nasıl kullanılacağına dair oldukça ayrıntılı bir şekilde anlatılan bir dizi örnek var.

Geçen hafta, ConcurrentDictionary'ı 32 iş parçacığı kullanarak I / O gerçekleştirmek için test ederek geçirdim. Reklamı yapıldığı gibi çalışıyor gibi görünüyor, bu da muazzam miktarda test yapıldığını gösteriyor.

Düzenleme : .NET 4 ConcurrentDictionary ve desenler.

Microsoft, Patterns of Paralell Programming adlı bir pdf yayınladı. .Net 4 Eşzamanlı uzantılar için kullanılacak doğru kalıpları ve kaçınılması gereken anti kalıpları gerçekten güzel ayrıntılarda açıklandığı gibi indirmeye değer. İşte burada.


4

Temel olarak yeni ConcurrentDictionary ile gitmek istersiniz. Kutudan çıkar çıkmaz iş parçacığı güvenli programlar yapmak için daha az kod yazmanız gerekir.


Concurrent Dictionery ile devam edersem herhangi bir sorun olur mu?
kbvishnu

1

ConcurrentDictionaryO yerine getirmesi halinde mükemmel bir seçenektir tüm senin iş parçacığı emniyet ihtiyaçlarını. Değilse, başka bir deyişle biraz karmaşık bir şey yapıyorsanız, normal bir Dictionary+ lockdaha iyi bir seçenek olabilir. Örneğin, bir sözlüğe bazı siparişler eklediğinizi ve siparişlerin toplam miktarını güncel tutmak istediğinizi varsayalım. Şu şekilde kod yazabilirsiniz:

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;
}

Bu kod iş parçacığı için güvenli değil. _totalAmountAlanı güncelleyen birden fazla iş parçacığı, alanı bozuk bir durumda bırakabilir. Bu nedenle, aşağıdakilerle korumayı deneyebilirsiniz lock:

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

Bu kod "daha güvenlidir", ancak yine de iş parçacığı için güvenli değildir. _totalAmountSözlükteki girişlerle tutarlılığının garantisi yoktur . Başka bir iş parçacığı, bir UI öğesini güncellemek için bu değerleri okumayı deneyebilir:

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

totalAmountSözlükte siparişlerin sayımına karşılık gelmeyebilir. Görüntülenen istatistikler yanlış olabilir. Bu noktada, lockkorumayı sözlüğün güncellenmesini içerecek şekilde genişletmeniz gerektiğini anlayacaksınız :

// 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);

Bu kod tamamen güvenlidir, ancak a kullanmanın tüm faydaları ConcurrentDictionaryortadan kalkmıştır.

  1. Performans, normal kullanmaktan daha kötü hale geldi Dictionary, çünkü içindeki dahili kilitleme ConcurrentDictionaryartık savurgan ve gereksiz.
  2. Paylaşılan değişkenlerin korumasız kullanımları için tüm kodunuzu incelemelisiniz.
  3. Garip bir API ( TryAdd?, AddOrUpdate?) Kullanmak zorunda kaldınız .

Bu yüzden benim tavsiyem: Dictionary+ ile başlayın lockve daha sonra ConcurrentDictionarybir performans optimizasyonu olarak yükseltme seçeneğini koruyun, eğer bu seçenek gerçekten uygunsa. Çoğu durumda olmayacak.


0

Biz benzer her 1 saatte yeniden doldurulan ve sonra birden çok istemci iş parçacığı tarafından okunan önbellekli koleksiyonu için ConcurrentDictionary, kullandım çözümüne yönelik bu örnek iplik güvenli mi? soru.

ReadOnlyDictionary olarak değiştirmenin  genel performansı iyileştirdiğini gördük  .


ReadOnlyDictionary sadece başka bir ID sözlüğünün etrafındaki sarmalayıcıdır. Kaputun altında ne kullanıyorsun?
Lu55

@ Lu55, MS .Net kitaplığını kullanıyorduk ve mevcut / uygulanabilir sözlükler arasından seçim yaptık. ReadOnlyDictionary
Michael Freidgeim'in
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.