spinlock 仅适用于临界区在几个 cpu 周期内完成的极短操作,禁止 io、阻塞、堆分配及复杂计算;需配合 volatile/interlocked 保证可见性,且必须实测验证耗时是否低于 500 纳秒。

SpinLock 不是“更轻量的 lock”,它只在临界区执行时间
SpinLock.Enter 必须配对 Exit,且 lockTaken 是唯一安全判断依据
SpinLock 是 struct,没有析构逻辑,Exit() 不会自动调用。漏掉或重复调用都会导致未定义行为——最常见的是死锁或 InvalidOperationException: The calling thread does not own the lock(启用 owner tracking 时)。
-
Enter(ref bool lockTaken)是唯一合法入口,不能直接传true或硬编码判断 -
lockTaken必须在try外声明,且finally中仅当它为true才调用Exit() - 禁止把
SpinLock实例存进readonly字段:struct 赋值会复制,导致多个副本各自维护独立状态,锁失效 - 不要跨线程共享同一个
SpinLock实例引用——不是线程安全的,它的线程所有权检查依赖当前线程 ID
SpinWait.SpinOnce 和 SpinUntil 不是“优雅等待”,而是手动控制退避节奏
SpinWait 本质是带自适应退避的忙等计数器,不是通用等待工具。它内部按次数做三档退避:SpinOnce() 到第 10 次后开始 Thread.Yield(),每 5 次再加一次 Thread.Sleep(0),每 20 次加一次 Thread.Sleep(1)。但它不会帮你读共享变量,也不会自动重试。
-
SpinUntil(() => flag)看似方便,但若flag是普通字段而非volatile或用Interlocked更新,很可能永远等不到——编译器/CPU 可能缓存旧值 - 别在循环里反复 new
SpinWait:它虽是 struct,但每次新建都重置计数器,失去自适应意义 - 一个
SpinWait实例只能被单个线程使用;多线程并发调用同一个实例的SpinOnce会导致计数错乱
SpinLock 绝对不能用于 IO、网络、文件操作或任何可能阻塞的临界区
SpinLock 的设计前提就是“锁内代码必须在几个 CPU 周期内完成”。一旦里面出现哪怕一次 await、File.Read、HttpClient.GetAsync 或 Thread.Sleep,CPU 就会原地空转到天荒地老,且无法被调度器中断。
- 临界区中禁止调用任何托管堆分配操作(如
new object()),GC 可能触发 STW,导致其他线程无限自旋 - 避免在锁内做算术复杂度不可控的操作,比如未限定长度的字符串拼接、未经剪枝的遍历
- 如果临界区平均耗时超过 500 纳秒,就该怀疑 SpinLock 是否合适;实测建议用
Stopwatch在热路径打点,而不是凭感觉
SpinLock 和 SpinWait 都不解决可见性问题,得靠 volatile / Interlocked / MemoryBarrier
自旋本身不提供内存屏障语义。SpinLock.Enter 会在获取锁时插入 acquire fence,Exit 插入 release fence,但这只覆盖锁进出点。锁内的变量读写仍可能被重排序或缓存。
- 共享标志位如
isReady必须声明为volatile,或用Interlocked.CompareExchange更新,否则自旋线程可能永远读不到变化 - 不要指望
SpinWait的SpinOnce自动刷新缓存;它不做任何读操作,只是“让出一点时间” - 若需在 SpinLock 外暴露状态,推荐用
Interlocked系列函数更新,比 volatile + lock 组合更轻量也更明确
真正难的从来不是写对 Enter/Exit,而是判断“这段代码到底够不够短、争用到底够不够低”——这没法靠文档说清,得压测、看 CPU 使用率曲线、查 ETW 事件里的自旋等待时长。SpinLock 的坑不在 API,而在它逼你直面并发性能的本质代价。











