c# ConcurrentDictionary的GetOrAdd和AddOrUpdate是原子操作吗

来源:这里教程网 时间:2026-02-21 17:39:48 作者:

是的,

GetOrAdd
AddOrUpdate
都是原子操作 —— 但仅限于“键存在性判断 + 值获取/插入/更新”这一整条逻辑路径在内部完成,不包括你传入的委托(
Func
)执行过程。

为什么说“是原子操作”,但又得小心委托?

这两个方法的原子性体现在:它们会先检查键是否存在,再决定是读取现有值、还是调用你提供的工厂函数生成新值、或调用更新函数计算新值 —— 这个“检查+分支决策+写入”三步,在

ConcurrentDictionary
内部由底层分段锁或无锁算法保障不会被其他线程打断。

但注意:

GetOrAdd(key, factory)
中的
factory
,或
AddOrUpdate(key, addVal, updateFactory)
中的
updateFactory
,是在锁外执行的。这意味着:

如果工厂函数很慢、有副作用(比如发 HTTP 请求、写文件),它可能被多个线程并发执行多次(即使最终只有一份结果被写入); 如果工厂函数依赖外部状态(如静态变量、共享对象),仍需自行保证该状态的线程安全; 它不是“整个表达式原子”,而是“键操作原子 + 工厂可重入”。

什么时候该用
GetOrAdd
,什么时候选
AddOrUpdate

看你的业务逻辑是否需要区分“首次添加”和“后续更新”:

GetOrAdd(key, value)
:适合缓存场景,比如“查用户配置,没有就用默认值初始化”,值是静态或轻量构造的;
GetOrAdd(key, factory)
:适合值创建开销大或需上下文(如
key => new ExpensiveObject(key)
),且你接受工厂可能被重复调用;
AddOrUpdate(key, addVal, updateFactory)
:适合计数器、聚合类场景,比如“用户点击次数+1”,必须保证每次更新都基于最新值(
updateFactory
会收到当前值,避免脏读);
别用
ContainsKey
+
Add
组合 —— 这不是原子的,竞态条件直接导致
ArgumentException
或数据丢失。

常见错误:以为委托里能安全改共享状态

下面这段代码看似安全,实则危险:

var counter = new ConcurrentDictionary<string, int>();
int sharedTotal = 0;
counter.AddOrUpdate("hits", 1, (k, v) => {
    sharedTotal += 1; // ❌ 多线程并发执行,sharedTotal 会丢加法
    return v + 1;
});

正确做法是把聚合逻辑留在委托外,或改用线程安全类型(如

Interlocked.Increment
):

counter.AddOrUpdate("hits", 1, (k, v) => v + 1); // ✅ 纯计算,安全
Interlocked.Increment(ref sharedTotal); // ✅ 如真需更新共享计数器

真正容易被忽略的点在于:原子性只管字典结构本身,不管你的委托干了什么。一旦委托里有 IO、锁、共享变量修改或非幂等操作,就得自己兜底 ——

ConcurrentDictionary
不会替你管这些。

相关推荐