++在多线程中必然漏值,因其非原子:读→加1→写回三步可被中断;interlocked.increment、add、compareexchange基于cpu原子指令(如lock xadd),通过ref参数确保内存地址操作,提供真正原子性、无锁、线程安全的计数与cas。

别用 ++ 或 -- 做多线程计数,它一定会漏值;直接上 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 int):只支持 +1,语义明确,JIT 可能做微优化,适合请求计数、命中统计等固定步长场景 -
Interlocked.Add(ref int, int):支持任意整数增量(含负数),单位建议统一为“分”或“毫秒”,避免浮点;不要用它实现 +1,语义弱且无收益 - 别写
Interlocked.Add(ref x, 1)来模拟 +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 value = 42; int result = Interlocked.Add(value, 1);❌ 编译失败:期望ref int - 正确做法:
int value = 42; int result = Interlocked.Add(ref value, 1);✅ - 确保操作的是类字段或局部变量,且类型为
int、long、IntPtr、object或泛型T(.NET 6+) - 避免在属性 getter 中包装
Interlocked调用——这掩盖了线程安全边界,也容易误以为“属性本身是线程安全的”
最常被忽略的一点:所有 Interlocked 方法都不抛异常,但传入 null 引用会触发 NullReferenceException;而 CompareExchange<t></t> 对泛型 T 的约束很严格——.NET 6+ 要求它必须是不可变引用类型或纯值类型,否则运行时行为未定义。











