应使用 interlocked.increment 实现线程安全的整数递增,因其底层映射 cpu 的 lock xadd 指令,保证原子性;count++ 非原子,会导致确定性丢失。

直接用 Interlocked.Increment,别写 count++,也别包 lock——它就是为这种单步整数递增而生的原子操作。
为什么 count++ 在多线程里必然出错
它不是一条指令,而是三步:读 count 值 → 加 1 → 写回。两个线程几乎同时执行,可能都读到 5,各自加成 6,再写回去,最终结果还是 6,而不是预期的 7。这不是概率问题,是确定性丢失。
常见错误现象:Task.Run(() => counter++).Wait() 并发跑 100 次,counter 最终只有 60 多;有人加 volatile,但那只能保证“改了别人能看到”,不保证“改的动作本身不被覆盖”,照样错。
要点:
-
Interlocked.Increment底层映射到 CPU 的LOCK XADD或XADD指令,在 x86/x64 上天然原子 - 必须带
ref:写成Interlocked.Increment(ref counter),漏掉ref会编译失败 - 返回的是**递增后的值**(比如原值是 9,返回 10),不用再读一遍变量
Interlocked.Increment 和 Interlocked.Add 该怎么选
如果每次只加 1,就用 Increment;如果要加任意数(比如按请求字节数累加权重),就用 Add。两者性能几乎无差别,但语义更清晰。
示例对比:
int counter = 0; // ✅ 推荐:加 1,语义明确,返回新值 int afterInc = Interlocked.Increment(ref counter); // 返回 1 // ✅ 推荐:加 5,同样原子、返回新值 int afterAdd = Interlocked.Add(ref counter, 5); // 返回 6 // ❌ 不推荐:写两行 Increment,语义冗余,且没比 Add(ref, 2) 快 Interlocked.Increment(ref counter); Interlocked.Increment(ref counter);
注意:Interlocked.Add 支持 int、long、uint、ulong、IntPtr;不支持 double 或自定义类型。
读取计数值时要不要用 Interlocked
单纯读 int 在 x86/x64 上是原子的(对齐前提下),但存在可见性风险:一个线程刚写完,另一个线程可能还在用寄存器里的旧值。所以裸读 counter 不安全。
安全做法有三种:
- 用
Interlocked.CompareExchange(ref counter, 0, 0)—— 它不改值,只读并带内存栅栏,开销略大但最稳妥 - 如果读操作本就在临界区里(比如和写一起被
lock包着),那直接读没问题,但这就偏离了用Interlocked的初衷 - 避免用
volatile int counter+counter++组合——volatile救不了原子性
别在不该用锁的地方硬上 lock
有人为“以后可能要加逻辑”提前套 lock,结果把高频累加拖慢 3–5 倍。实测在百万级并发下,Interlocked.Increment 吞吐远高于 lock,因为没上下文切换、没等待队列、没竞争唤醒开销。
该用 lock 的场景其实很窄:
- 需要同时更新多个变量(比如
sum和count要同步变) - 累加前要判断条件(如“仅当
counter 才加”),这时得靠 <code>Interlocked.CompareExchange循环实现 CAS - 涉及非原子类型操作(比如修改
List<t></t>或对象字段组合)
真正容易被忽略的点:Interlocked 系列方法不抛异常、不阻塞、不参与死锁检测——它快得像一条 CPU 指令,但也意味着你没法靠它做复合逻辑。一旦需求越界,就得换同步模型,不能硬撑。











