1. 程式人生 > >.NET的ConcurrentDictionary,執行緒安全集合類

.NET的ConcurrentDictionary,執行緒安全集合類

ConcurrentDictionary 是.NET 4.0裡面新增的號稱執行緒安全的集合類。

那麼自然,我們會預期ConcurrentDictionary 中的程式碼是執行緒安全的(至少幾個關鍵方法是執行緒安全的).

舉個例子,使用者可能會預期GetOrAdd中的方法當Key不存在的時候只執行一次Add的委託,第二次呼叫GetOrAdd就應該直接取回剛才生成的值了.

參考一下以下程式碼:

複製程式碼
        public static  void Test()
        {
            var concurentDictionary = new ConcurrentDictionary<int
, int>(); var w = new ManualResetEvent(false); int timedCalled = 0; var threads = new List<Thread>(); for (int i = 0; i < Environment.ProcessorCount; i++) { threads.Add(new Thread(() => { w.WaitOne(); concurentDictionary.GetOrAdd(1
, i1 => { Interlocked.Increment(ref timedCalled); return 1; }); })); threads.Last().Start(); } w.Set();//release all threads to start at the same time
Thread.Sleep(100); Console.WriteLine(timedCalled);// output is 4, means call initial 4 times //Console.WriteLine(concurentDictionary.Keys.Count); }
複製程式碼

GetOrAdd方法的定義就是按照Key獲取一個Value,如果Key不存在,那麼呼叫Func<T> 新增一個鍵值對.

按照ConcurrentDictionary的定義,  我預期這個Add應該只被呼叫一次

可是上面那段程式碼的執行結果表明, Interlocked.Increment(ref timedCalled); 被呼叫了4次,真是尷尬啊

用於初始化值的委託還真的是可以多次執行的,所以

  • 要麼保證委託中的程式碼重複執行不會有問題
  • 要麼使用執行緒安全的初始化方法,例如Lazy<T> 
複製程式碼
 public static void Test()
        {
            var concurentDictionary = new ConcurrentDictionary<int, int>();

            var w = new ManualResetEvent(false);
            int timedCalled = 0;
            var threads = new List<Thread>();
            Lazy<int> lazy = new Lazy<int>(() => { Interlocked.Increment(ref timedCalled); return 1; });
            for (int i = 0; i < Environment.ProcessorCount; i++)
            {
                threads.Add(new Thread(() =>
                {
                    w.WaitOne();
                    concurentDictionary.GetOrAdd(1, i1 =>
                    {
                        return lazy.Value;
                    });
                }));
                threads.Last().Start();
            }

            w.Set();//release all threads to start at the same time                     Thread.Sleep(100);
            Console.WriteLine(timedCalled);// output is 1        }
複製程式碼

附: 註釋中也不說一下這個初始化方法會被多次呼叫,如果不是偶然遇到這個問題,估計永遠都不知道

複製程式碼
 //// Summary:
        //     Adds a key/value pair to the System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue>
        //     if the key does not already exist.
        //// Parameters:
        //   key:
        //     The key of the element to add.
        ////   valueFactory:
        //     The function used to generate a value for the key
        //// Returns:
        //     The value for the key. This will be either the existing value for the key
        //     if the key is already in the dictionary, or the new value for the key as
        //     returned by valueFactory if the key was not in the dictionary.
        //// Exceptions:
        //   System.ArgumentNullException:
        //     key is a null reference (Nothing in Visual Basic).-or-valueFactory is a null
        //     reference (Nothing in Visual Basic).
        ////   System.OverflowException:
        //     The dictionary contains too many elements.
複製程式碼

該集合類中所有使用Func<T>的方法也存在類似的問題

希望能給還不知道該問題的朋友提個醒,避免不必要的BUG