C# 中 ConcurrentDictionary 必定線程安全嗎?

根據 .NET 官方文檔的定義:ConcurrentDictionary<TKey,TValue> Class 表示可由多個線程同時訪問的線程安全的鍵/值對集合。這也是咱們在併發任務中比較經常使用的一個類型,但它真的是絕對線程安全的嗎?api

仔細閱讀官方文檔,咱們會發如今文檔的底部線程安全性小節裏這樣描述:數組

ConcurrentDictionary<TKey,TValue> 的全部公共和受保護的成員都是線程安全的,可從多個線程併發使用。可是,經過一個由 ConcurrentDictionary<TKey,TValue> 實現的接口的成員(包括擴展方法)訪問時,不保證其線程安全性,而且可能須要由調用方進行同步。安全

也就是說,調用 ConcurrentDictionary 自己的方法和屬性能夠保證都是線程安全的。可是因爲 ConcurrentDictionary 實現了一些接口(例如 ICollection、IEnumerable 和 IDictionary 等),使用這些接口的成員(或者這些接口的擴展方法)不能保證其線程安全性。System.Linq.Enumerable.ToList 方法就是其中的一個例子,該方法是 IEnumerable 的一個擴展方法,在 ConcurrentDictionary 實例上使用該方法,當它被其它線程改變時可能拋出 System.ArgumentException 異常。下面是一個簡單的示例:併發

static void Main(string[] args)
{
    var cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd.AddOrUpdate(value, value, (key, oldValue) => value);
        }
    });

    while (true)
    {
        cd.ToList(); //調用 System.Linq.Enumerable.ToList,拋出 System.ArgumentException 異常
    }
}

System.Linq.Enumerable.ToList 擴展方法:dom

System.Linq.Enumerable.ToList

發生異常是由於擴展方法 ToList 中調用了 List 的構造函數,該構造函數接收一個 IEnumerable<T> 類型的參數,且該構造函數中有一個對 ICollection<T> 的優化(由 ConcurrentDictionary 實現的)。函數

System.Collections.Generic.List<T> 構造函數:優化

System.Collections.Generic.List

List 的構造函數中,首先經過調用 Count 獲取字典的大小,而後以該大小初始化數組,最後調用 CopyTo 將全部 KeyValuePair 項從字典複製到該數組。由於字典是能夠由多個線程改變的,在調用 Count 後且調用 CopyTo 前,字典的大小能夠增長或者減小。當 ConcurrentDictionary 試圖訪問數組超出其邊界時,將引起 ArgumentException 異常。線程

ConcurrentDictionary<TKey,TValue> 中實現的 ICollection.CopyTo 方法:
ConcurrentDictionary-CopyTo-ArgumentExceptioncode


若是您只須要一個包含字典全部項的單獨集合,能夠經過調用 ConcurrentDictionary.ToArray 方法來避免此異常。它完成相似的操做,可是操做以前先獲取了字典的全部內部鎖,保證了線程安全性。blog

ConcurrentDictionary-ToArray

注意,不要將此方法與 System.Linq.Enumerable.ToArray 擴展方法混淆,調用 Enumerable.ToArrayEnumerable.ToList 同樣,可能引起 System.ArgumentException 異常。

看下面的代碼中:

static void Main(string[] args)
{
    var cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd.AddOrUpdate(value, value, (key, oldValue) => value);
        }
    });

    while (true)
    {
        cd.ToArray(); //ConcurrentDictionary.ToArray, OK.
    }
}

此時調用 ConcurrentDictionary.ToArray,而不是調用 Enumerable.ToArray,由於後者是一個擴展方法,前者重載解析的優先級高於後者。因此這段代碼不會拋出異常。

可是,若是經過字典實現的接口(繼承自 IEnumerable)使用字典,將會調用 Enumerable.ToArray 方法並拋出異常。例如,下面的代碼顯式地將 ConcurrentDictionary 實例分配給一個 IDictionary 變量:

static void Main(string[] args)
{
    System.Collections.Generic.IDictionary<int, int> cd = new ConcurrentDictionary<int, int>();
    Task.Run(() =>
    {
        var random = new Random();
        while (true)
        {
            var value = random.Next(10000);
            cd[value] = value;
        }
    });

    while (true)
    {
        cd.ToArray(); //調用 System.Linq.Enumerable.ToArray,拋出 System.ArgumentException 異常
    }
}

此時調用 Enumerable.ToArray,就像調用 Enumerable.ToList 時同樣,引起了 System.ArgumentException 異常。

總結

正如官方文檔上所說的那樣,ConcurrentDictionary 的全部公共和受保護的成員都是線程安全的,可從多個線程併發調用。可是,經過一個由 ConcurrentDictionary 實現的接口的成員(包括擴展方法)訪問時,並非線程安全的,此時要特別注意。

若是須要一個包含字典全部項的單獨集合,能夠經過調用 ConcurrentDictionary.ToArray 方法獲得,千萬不能使用擴展方法 ToList,由於它不是線程安全的。


參考:


做者 : 技術譯民
出品 : 技術譯站

相關文章
相關標籤/搜索