【不小心就會犯錯】 .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
浙公網安備 33010602011771號