c# Interlocked 和 lock 的区别 c#无锁编程怎么实现

Interlocked专治单个变量的原子读写,lock用于多步逻辑的排他执行;前者无锁高效,后者支持复杂临界区,误用会导致性能下降或并发错误。

什么时候该用 Interlocked,而不是 lock

直接说结论:Interlocked 专治「单个变量的原子读写」,比如计数器增减、标志位设置、引用替换;lock 是为「一段逻辑的排他执行」准备的,比如更新多个字段、读-改-写复合操作、涉及 I/O 或复杂校验的临界区。

常见错误现象:有人把 Interlocked.Increment(ref _count) 换成 lock 去保护一个 _count++,结果性能掉一截还毫无必要;也有人试图用 Interlocked 去保证「先检查余额再扣款」这种两步操作的原子性,结果出现超卖——因为 Interlocked 不提供「条件+动作」的原子组合能力。

  • Interlocked 底层靠 CPU 原子指令(如 LOCK XADD),无锁、无上下文切换、无等待,是真正的「无锁编程」基础
  • lock 底层调用 Monitor.Enter/Exit,会触发内核态同步原语,线程争抢失败时可能挂起、调度,开销明显更高
  • 只要操作不超出「一个变量 + 一种原子语义」,就优先选 Interlocked;一旦涉及多个变量、判断分支、非原子表达式(如 x = x * 2 + 1),lockMonitor 就不可替代

Interlocked.CompareExchange 是无锁编程的核心入口

C# 的无锁编程不是靠“不用锁”来定义的,而是靠「CAS(Compare-And-Swap)循环重试」实现的乐观并发控制。而 Interlocked.CompareExchange 就是这个机制的唯一公开出口。

它签名是:Interlocked.CompareExchange(ref int location1, int value, int comparand) —— 只有当 location1 == comparand 时,才把 value 写入,并返回旧值;否则不写,只返回当前值。整个过程原子。

  • 典型用法是自旋写入:反复读取当前值 → 计算新值 → CAS 尝试更新 → 失败则重试
  • 不能用于任意对象:只能用

    intlongIntPtr、引用类型(object 或泛型 T where T : class)
  • 注意内存屏障:CompareExchange 默认带 full memory barrier,但若需更细粒度(如仅 acquire/release),得用 Interlocked.CompareExchange(ref T, T, T, MemoryOrder)(.NET 8+)
private int _state = 0; // 0=ready, 1=busy
public bool TryEnter()
{
    return Interlocked.CompareExchange(ref _state, 1, 0) == 0;
}
// 成功返回 true,且 _state 已设为 1;失败说明已被别人抢先设为 1

lockInterlocked 性能差距有多大?

在高竞争、高频更新场景下,差距非常真实:10 个线程对同一计数器做 100 万次递增,Interlocked.Increment 通常比 lock 快 3–5 倍,且 CPU 时间更稳,不会因线程挂起/唤醒抖动。

但这个优势只在「简单操作」上成立。一旦你把 Interlocked 套进复杂逻辑里强行“无锁”,比如在 CAS 循环里调用数据库、做字符串拼接、访问非原子字段,性能反而更差——因为自旋浪费 CPU,且逻辑本身已失去原子性保障。

  • 别为了“无锁”而无锁:无锁 ≠ 更快,而是「适合场景时更轻量」
  • Interlocked 操作本身不会阻塞,但你的业务逻辑如果包含阻塞点(如 await、File.Read、Thread.Sleep),那整个方案就不再是无锁了
  • 真要压榨性能,可搭配 SpinLock(短临界区)、ReaderWriterLockSlim(读多写少)、或 Channels(生产者-消费者解耦)

最容易被忽略的坑:Interlocked 不保顺序,也不保可见性之外的语义

很多人以为用了 Interlocked 就万事大吉,结果发现日志乱序、状态跳变、甚至偶发 null 引用——问题往往出在「它只管那个变量本身,不管其他东西」。

  • Interlocked.Exchange(ref obj, newObj) 确保引用替换原子,但不保证 newObj 的构造已完成(即可能看到部分初始化的对象),除非你在构造后才交换
  • 多个 Interlocked 调用之间**没有顺序保证**:A 线程执行 Interlocked.Increment(ref x)Interlocked.Increment(ref y),B 线程可能看到 y 先变、x 后变
  • 它不阻止编译器/CPU 重排序:若需严格顺序(比如先写数据、再设就绪标志),得配合 Thread.MemoryBarrier() 或用 volatile 字段(但 volatileInterlocked 混用需格外小心)

真正难的从来不是“怎么写无锁”,而是“怎么证明它在所有路径下都正确”。哪怕只多一步判断,就很可能得退回 lock