++在多线程中不安全,因其拆分为读值、加1、写回三步非原子操作,导致竞态漏计数;应使用Interlocked.Increment/Add等原子方法,注意适用范围与部署边界。
为什么 ++ 在多线程里不安全
因为 ++ 不是原子操作:它实际拆成「读值 → 加1 → 写回」三步。两个线程同时读到同一个旧值,各自加1再写回,结果只+1而不是+2。这不是偶发bug,是必然发生——只要没同步,就漏计数。
常见错误现象:Task.Run(() => count++).Wait() 跑100次,count 最终远小于100;用 lock 能解决但吞吐低;有人试图用 volatile,但它只保可见性,不保原子性,照样错。
- 适用场景:高频更新的共享计数器(如请求统计、限流计数、缓存命中计数)
- 别对非整型变量(如
double、自定义对象)用Interlocked原子操作——它只支持int、long、IntPtr等少数类型 - 性能影响极小:比
lock快5–10倍,底层直接映射到 CPU 的XADD或LOCK XCHG指令
Interlocked.Increment 和 Interlocked.Add 怎么选
Interlocked.Increment 只能 +1,Interlocked.Add 支持任意整数增减。两者都返回操作后的最新值,这点很重要——你不需要再读一次变量。
示例:
int count = 0; // 正确:拿到新值,且线程安全 int newValue = Interlocked.Increment(ref count); // 返回 1 // 正确:加5,也返回新值 int afterAdd = Interlocked.Add(ref count, 5); // 返回 6 // 错误:不要这样链式调用,语义不清且无必要 Interlocked.Increment(ref count); Interlocked.Increment(ref count); // 写两行不如用 Add(ref count, 2)
- 如果只是累加固定步长(比如每次+1),优先用
Increment/Decrement,语义更直白 - 如果步长可变(如按请求大小加权计数),必须用
Add - 注意 ref 修饰符不能省:
Interlocked.Increment(ref count),传值进去会编译报错
读取计数值时要不要加锁或用 Interlocked
单纯读取 int 是原子的(x86/x64 上读写 32 位对齐变量天然原子),但存在可见性问题:一个线程改了值,另一个线程可能还在用寄存器里的旧副本。
所以不建议裸读。有三种安全做法:
- 用
Volatile.Read(ref count)—— 轻量,强制从内存读,不重排指令 - 用
Interlocked.CompareExchange(ref count, 0, 0)—— 返回当前值,顺便带内存栅栏,稍重但通用 - 如果读操作本身就在临界区里(比如和写一起被
lock包着),那直接读没问题,但这就失去用Interlocked的意义了
别用 volatile int count 字段然后直接 count++——volatile 不救原子性,只救可见性。
跨进程或跨 AppDomain 怎么计数
Interlocked 只作用于同一进程内的内存地址,对文件、数据库、Redis、甚至另一个 .NET 进程完全无效。如果你的应用是多实例(如 IIS 多 worker 进程、Kestrel 多实例),Interlocked 对它们之间毫无意义。
这时候得换方案:
- 用分布式锁 + 数据库自增字段(如 PostgreSQL 的
nextval()) - 用 Redis 的
INCR命令——它本身就是服务端原子操作 - 用内存映射文件(
MemoryMappedFile)配合Interlocked,仅限同一台机器的多个进程,且需手动处理初始化和清理
最容易被忽略的一点:你以为在“多线程”里用 Interlocked 就万事大吉,但其实部署模型早把你带进“多进程”甚至“多机器”的坑里了——先确认你的并发边界在哪,再决定用哪层原子性。









