14

我的團隊目前正在討論這個問題。字典上的雙重鎖定鎖「ContainsKey」

有問題的代碼是沿

if (!myDictionary.ContainsKey(key)) 
{ 
    lock (_SyncObject) 
    { 
     if (!myDictionary.ContainsKey(key)) 
     { 
      myDictionary.Add(key,value); 
     } 
    } 
} 

線的東西有些是我見過的帖子說,這可能是一個很大的NO NO(使用TryGetValue時)。然而我們團隊的成員說可以,因爲「ContainsKey」不會對密鑰集合進行迭代,而是通過O(1)中的哈希代碼檢查密鑰是否被包含。因此他們聲稱這裏沒有危險。

我希望得到你對這個問題的誠實意見。

+0

你可能想看看ConcurrentDictionary。 – chillitom 2011-05-16 14:22:56

+1

只是一個細節,但你可能意味着'!ContainsKey()' – 2011-05-16 14:25:13

+0

你是否發現鎖定整個字典的效率問題? – Nick 2011-05-16 14:29:33

回答

24

不要這樣做。這並不安全。

您可以從一個線程調用ContainsKey,而另一個線程調用Add。這根本不受Dictionary<TKey, TValue>的支持。如果Add需要重新分配桶等,我可以想象你可能得到一些非常奇怪的結果,或一個例外。它可能已經寫成這樣一種方式,你沒有看到任何討厭的影響,但我不想依靠它。

它的使用有一點雙重檢查鎖定爲簡單的讀/寫操作的領域,雖然我還是反對它 - 它的另一個撥打電話到已經明確地描述爲是安全的API多個併發呼叫。

如果你在.NET 4上,ConcurrentDictionary可能是前進的方向。否則,只需鎖定每個訪問。

+0

如果在應用程序中訪問了很多代碼,該怎麼辦?那麼這可能是一個性能問題。除了ConcurrentDictionary之外,還有其他的方法可以做到嗎? – JarJarrr 2011-05-16 14:34:46

+1

@Amir:我寧願承受比應用程序爆炸小的性能損失。你是否分析了應用程序以查看這是否導致* actual *性能問題?未經驗證的鎖很便宜:不要開始嘗試推出自己的無鎖代碼,而不會證明您真的需要它。 – 2011-05-16 14:36:35

+0

我正確地假設,如果我去訪問而不是DCL的鎖,那麼我將需要鎖定每個方法的每一個訪問到這個字典? – JarJarrr 2011-05-16 14:40:53

6

如果您處於多線程環境中,您可能更願意使用ConcurrentDictionary。幾個月前我在博客上發表了一篇文章,您可能會發現這篇文章很有用:http://colinmackay.co.uk/blog/2011/03/24/parallelisation-in-net-4-0-the-concurrent-dictionary/

+0

如果使用.Net 4.0不是一個選項,什麼是這個問題的充足的解決方案? (使用3.5) – JarJarrr 2011-05-16 14:29:37

+1

很好的選擇和文章,但它並沒有真正回答建議的鎖定是否「OK」。 – 2011-05-16 14:33:25

5

此錯誤信息不正確。 Dictionary<TKey, TValue>類型不支持同時讀取和寫入操作。儘管在鎖內調用了Add方法,但ContainsKey不是。因此,它很容易違反同時讀/寫規則,並將導致您的實例損壞

1

它看起來不是線程安全的,但它可能很難使其失敗。

迭代vs散列查找參數不成立,例如可能存在散列衝突。

0

如果這本字典很少被編寫和經常閱讀,那麼我通常會使用安全的雙重鎖定,通過在寫入時替換整個字典。如果您可以批量寫入以減少頻率,這一點尤其有效。

例如,這是一個我們使用的方法的簡化版本,它試圖獲取與某個類型關聯的模式對象,如果它不能,那麼它繼續併爲所有類型創建模式對象發現在相同的組件指定類型以最小化的次數整個詞典必須被複制:因爲有組件多次

public static Schema GetSchema(Type type) 
    { 
     if (_schemaLookup.TryGetValue(type, out Schema schema)) 
      return schema; 

     lock (_syncRoot) { 
      if (_schemaLookup.TryGetValue(type, out schema)) 
       return schema; 

      var newLookup = new Dictionary<Type, Schema>(_schemaLookup); 

      foreach (var t in type.Assembly.GetTypes()) { 
       var newSchema = new Schema(t); 
       newLookup.Add(t, newSchema); 
      } 

      _schemaLookup = newLookup; 

      return _schemaLookup[type]; 
     } 
    } 

因此,在這種情況下,詞典將被重建,頂多需要模式的類型。對於應用程序生命週期的其餘部分,字典訪問將是無鎖的。字典副本成爲程序集的一次性初始化成本。字典交換是線程安全的,因爲指針寫入是原子的,所以整個引用會立即切換。

您也可以在其他情況下應用相似的原則。