多线程计数必须用Interlocked方法而非++/--,因++非原子操作会导致漏值;Interlocked.Increment适用于+1场景,Add支持任意整数增减,CompareExchange是唯一CAS原语,所有操作均需ref变量地址。

别用 ++ 或 -- 做多线程计数,它一定会漏值;直接上 Interlocked.Increment、Interlocked.Add 或 Interlocked.CompareExchange,它们才是真原子。
为什么 ++ 在多线程里必然出错
它不是一条指令,而是三步:读内存 → 加1 → 写回。两个线程几乎同时执行时,都会读到同一个旧值,各自加1再写回,结果只+1。这不是“偶尔错”,是只要没同步就必现——哪怕只跑两次 Task.Run(() => count++),count 也可能卡在1不动。
- 别信
volatile int count+count++:volatile 只保可见性,不保原子性,照样漏 - 别把
Interlocked当“更安全的++”来用:它不接受表达式,只认ref变量地址 - 裸读
int虽然 x86/x64 上天然原子,但可能读到寄存器缓存旧值;要保证最新,得用Interlocked.Read(long)或加内存栅栏
Interlocked.Increment 和 Interlocked.Add 怎么选
两者都返回操作后的新值,且都要求 ref 参数;区别不在“是否原子”,而在适用场景和语义。
-
Interlocked.Increment(ref count):只支持 +1,语义明确,JIT 可能做微优化,适合请求计数、命中统计等固定步长场景 -
Interlocked.Add(ref balance, -50):支持任意整数增量(含负数),单位建议统一为“分”或“毫秒”,避免浮点;不要用它实现 +1,语义弱且无收益 - 别写
Interlocked.Increment(ref x); Interlocked.Increment(ref x);来模拟 +2:直接Add(ref x, 2)更干净 -
Interlocked.Add(ref long, long)在 .NET Core 2.0+ 已稳定,x86 下靠LOCK XADD保证 64 位原子性
Interlocked.CompareExchange 是唯一能做 CAS 的入口
它是无锁编程的基石,所有“检查后设置”逻辑都绕不开它:仅当当前值等于预期值时,才写入新值,并返回原值。
- 典型误用:
if (_state == 0) { Interlocked.CompareExchange(ref _state, 1, 0); }—— 判断和 CAS 之间有竞态窗口,必须靠返回值判断:if (Interlocked.CompareExchange(ref _state, 1, 0) == 0) { /* 真正设成功了 */ } - 支持
int、long、object和泛型T(.NET 6+,仅限不可变引用或纯值类型) - 初始化单例、构建无锁栈/队列、原子切换状态标志(如
IsRunning),全靠它 - 它不是“高级版 Add”,而是唯一能表达「读-改-写」原子性的原语;想自己实现原子减法?只能靠循环 +
CompareExchange
ref 不是语法糖,是硬性内存寻址要求
Interlocked 所有方法都依赖 CPU 原子指令(如 x86 的 XCHG、LOCK XADD),这些指令必须操作内存地址,而非值副本。
- 错误写法:
int v = 42; Interlocked.Increment(v);—— 编译失败:Cannot pass 'v' as a ref argument because it is not a variable - 正确写法:
int v = 42; Interlocked.Increment(ref v);—— 必须传变量本身地址 - 别对属性、索引器、struct 字段直接调用:
Interlocked.Increment(ref obj.Counter)合法,但Interlocked.Increment(ref obj.Data.Value)很可能因字段不可寻址而失败 - 别在属性 getter 里封装
Interlocked调用:这会误导调用方以为“读这个属性就是线程安全的”,实际只是掩盖了边界
真正容易被忽略的点是:CAS 循环里,CompareExchange 返回的是交换前的值,不是布尔结果;你得靠比对返回值和预期值来决定是否重试——这点写错,整个无锁逻辑就退化成随机行为。


















