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