将ConcurrentDictionary转为IDictionary时出现的异常并发行为
测试代码
using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; public class Program { public static void Main() { IDictionary<string, string> dictionary = new ConcurrentDictionary<string, string>(); // 有时会失败 // ConcurrentDictionary<string, string> dictionary = new ConcurrentDictionary<string, string>(); // 始终正常运行 Parallel.For(0, 10, i => dictionary.TryAdd("x1", "y1")); } }
抛出的异常
未处理的异常。System.AggregateException: 发生一个或多个错误。(字典中已存在该键)
System.ArgumentException: 字典中已存在该键。在 System.Collections.Concurrent.ConcurrentDictionary
2.System.Collections.Generic.IDictionary<TKey,TValue>.Add(TKey key, TValue value) 在 System.Collections.Generic.CollectionExtensions.TryAdd[TKey,TValue](IDictionary2 dictionary, TKey key, TValue value)
在 Program.<>c__DisplayClass0_0.b__0(Int32 i)
在 System.Threading.Tasks.Parallel.<>c__DisplayClass19_02.b__1(RangeWorker& currentWorker, Int64 timeout, Boolean& replicationDelegateYieldedBeforeCompletion) --- 上一位置的堆栈跟踪结束 --- 在 System.Threading.Tasks.Parallel.<>c__DisplayClass19_02.b__1(RangeWorker& currentWorker, Int64 timeout, Boolean& replicationDelegateYieldedBeforeCompletion)
在 System.Threading.Tasks.TaskReplicator.Replica.Execute()
--- 内部异常堆栈跟踪结束 ---
疑问
但如果不将ConcurrentDictionary转换为IDictionary类型,代码似乎始终能正常运行。请问这是什么原因?
原因解析
核心差异在于调用的TryAdd方法完全不同:
1. 转成IDictionary时调用的是通用扩展方法
当变量类型为IDictionary<string, string>时,调用的TryAdd是CollectionExtensions类的通用扩展方法,它的实现逻辑大致为:
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value) { if (!dictionary.ContainsKey(key)) { dictionary.Add(key, value); return true; } return false; }
这个逻辑在多线程环境下存在竞态条件:线程A检查到键不存在,还没执行Add时,线程B也会检查到键不存在;线程A先完成Add后,线程B再执行Add就会触发IDictionary.Add的接口约定——键已存在时抛出ArgumentException。而ConcurrentDictionary实现IDictionary.Add时严格遵循了这个约定,因此会抛出异常。
2. 直接用ConcurrentDictionary时调用的是专属实例方法
当变量类型为ConcurrentDictionary<string, string>时,调用的是ConcurrentDictionary自身的专属实例方法TryAdd。这个方法是为并发场景设计的,内部通过原子操作(锁或CAS机制)保证「检查键是否存在」和「添加键值对」是一个不可分割的原子操作,不会出现竞态条件。即使多线程同时调用,只会有一个线程添加成功,其他线程返回false,不会抛出任何异常。
内容的提问来源于stack exchange,提问作者Daniel Fath

