concurrentdictionary仅保证单方法原子性,组合操作需用tryadd等专用方法;containskey+赋值会引发竞态;addorupdate参数顺序错致逻辑错误;getoradd工厂函数可能重复执行;遍历时删元素会抛异常。

ConcurrentDictionary 不是 Dictionary 的线程安全平替,它只保证单个方法调用的原子性;组合操作(比如先查再写)必须换用 TryAdd、AddOrUpdate 等专用方法,否则照样出竞态 bug。
ContainsKey + 赋值组合为什么危险
这段代码看着合理,但实际会出问题:
if (!dict.ContainsKey("user123")) dict["user123"] = new User();
两个线程同时执行时,都通过 ContainsKey 检查(返回 false),然后都执行赋值——后一个覆盖前一个,且无法感知。更糟的是,如果 new User() 有副作用(如注册回调、发通知),会被重复触发。
-
ContainsKey本身线程安全,但它返回的布尔值一旦出来,就和后续操作脱钩了 - 只要它后面跟着任何写操作,整个逻辑就不是原子的
- 正确做法是改用
TryAdd("user123", new User()):内部一次完成“查不存在 + 插入”,失败就说明键已存在
AddOrUpdate 参数顺序错会导致旧值被无视
AddOrUpdate 的签名是 AddOrUpdate(TKey, Func<tkey tvalue>, Func<tkey tvalue>)</tkey></tkey>,即:
- 第二个参数:键不存在时调用的工厂函数(只接收
key) - 第三个参数:键存在时调用的更新函数(接收
key和oldValue)
传反了或语义混淆,结果就是每次调用都走新增分支:
// ❌ 错误:第二个参数写了双参 lambda,编译不过
dict.AddOrUpdate("k", (k, v) => v * 2, _ => 42);
<p>// ✅ 正确:不存在设为 42,存在时翻倍
dict.AddOrUpdate("k", _ => 42, (k, old) => old * 2);</p>
如果你只是想“存在就覆盖”,直接写 dict["k"] = newValue 更简洁——它本身线程安全,且不涉及旧值计算。
GetOrAdd 工厂函数可能重复执行
GetOrAdd 保证最终只有一个值被存入,但不保证工厂函数只执行一次。多个线程同时发现 key 缺失,会各自触发一次工厂函数:
- HTTP 请求发三遍
- 数据库连接建三次,其中两个立刻丢弃
- 日志里看到多次“正在加载配置”
解决办法不是加锁(破坏并发本意),而是让工厂函数本身幂等或延迟初始化:
dict.GetOrAdd("cfg", _ => new Lazy<config>(() => LoadConfigFromDb()));</config>
后续使用时取 .Value,Lazy<t></t> 内部已做线程安全初始化,只执行一次。注意:不要在工厂函数里写副作用逻辑(如写日志、发通知),除非你明确接受可能重复执行。
遍历时删元素会抛 InvalidOperationException
对 ConcurrentDictionary 做 foreach 是安全的,它返回的是某个时间点的快照;但只要在循环体内调用 TryRemove 或 Clear,运行时大概率抛 InvalidOperationException。
唯一安全路径是先收集要删的 key,再批量删除:
var keysToRemove = dict.Keys.Where(k => ShouldRemove(k)).ToList();
foreach (var key in keysToRemove)
{
dict.TryRemove(key, out _);
}
这个细节容易被忽略——因为枚举本身不崩溃,所以问题常在高并发压测或上线后才暴露。











