concurrentdictionary 通过原子操作避免多线程异常,而普通 dictionary 无任何线程安全防护,导致 add 冲突、遍历时修改、非原子读改写等错误;必须使用 addorupdate、getoradd 等内置原子方法,避免手写“先查后改”,委托需幂等,读操作为快照,count 非实时且低效。

直接用 ConcurrentDictionary 替代静态 Dictionary,就能避开“集合已修改”“键已存在”“索引越界”等绝大多数多线程写集合报错——它不是“加了锁就安全”,而是从设计上把读、写、更新、删除都做成原子操作。
为什么普通 Dictionary 在多线程下必崩
根本原因:它没做任何线程安全防护。两个线程同时调用 Add、一个线程遍历一个线程 Remove、甚至只是 dict[key]++ 这种看似简单的操作,都会触发 InvalidOperationException 或静默数据损坏。
-
Add遇到重复 Key 直接抛ArgumentException: An item with the same key has already been added. -
foreach中其他线程修改集合 → 立刻崩出Collection was modified -
dict[key] = value虽线程安全(底层是TryInsert),但dict[key]++不是——它先读、再算、再写,三步非原子,结果就是丢更新
ConcurrentDictionary 的安全写法:别碰“读-改-写”惯性
所有看似自然的“先查后改”逻辑,在并发环境下都是雷区。必须用它内置的原子方法替代手写逻辑。
- 要“有则更新、无则添加”?用
AddOrUpdate(key, addFactory, updateFactory),不要if (dict.ContainsKey(k)) dict[k] = v; else dict[k] = v; - 要“查无则初始化并存入”?用
GetOrAdd(key, factory),工厂函数只执行一次,哪怕多个线程同时触发 - 要“安全递增计数器”?别写
dict["cnt"]++,改用dict.AddOrUpdate("cnt", 1, (k, v) => v + 1) - 委托函数必须无副作用、轻量、幂等:不能改外部变量、不能发 HTTP 请求、不能依赖全局状态
读取和遍历时最容易忽略的坑
TryGetValue 和 foreach 是安全的,但“安全”不等于“实时”或“强一致”。它们返回的是某一时刻的快照,这点常被误读为 bug。
- 用
TryGetValue代替ContainsKey+dict[key]:前者一次哈希定位,后者两次,且中间可能被其他线程删掉该 Key - 遍历时别依赖
Keys或Values属性直接 foreach:它们返回的集合底层仍可被并发修改,建议先ToArray()再遍历 - 别信
Count:它是快照值,且内部是 O(N) 遍历链表计算,高并发下既不准又慢;判断空用IsEmpty - 遍历时调用
Clear()不会崩溃,但后续迭代项可能突然消失或重复——这不是异常,是设计使然
构造和性能优化的隐含条件
默认构造没问题,但如果你明确知道并发写线程数,可以提前指定 concurrencyLevel,减少锁争用。
-
new ConcurrentDictionary<string int>(concurrencyLevel: 8, capacity: 1024)</string>:concurrencyLevel是预估的**最大并发写线程数**,不是 CPU 核心数 -
capacity是初始桶数量,设太小会频繁扩容(虽线程安全,但影响性能),设太大浪费内存 - 读操作几乎无锁,所以读多写少场景下,它比加锁的
Dictionary快得多;但写冲突剧烈时(比如所有线程狂刷同一个 Key),性能会退化,此时应考虑分 Key 前缀或换结构 - 它不保证枚举顺序,也不维护插入顺序——这点和普通
Dictionary一致,别指望靠 foreach 输出顺序做逻辑依赖
最常被绕开的点:很多人以为只要用了 ConcurrentDictionary 就万事大吉,结果在委托里调用异步方法、修改共享状态、或者把 Count 当实时指标用,反而引入更难排查的竞态。它的安全是有前提的——你得尊重它的原子边界,不越界操作。











