很多今天看似理所当然的系统设计,都能在 Solaris 里找到早期而成熟的答案:Slab Allocator 让内核对象分配更高效,OpenZFS 把存储校验与管理能力推向新高度,DTrace 则改变了生产环境的可观测性思路。Turnstile 解决的是另一个经常被低估的问题:线程在竞争互斥锁时,怎样阻塞、唤醒并管理优先级,才能避免锁本身成为系统性能的瓶颈。
这类设计后来以不同形式出现在现代操作系统、编程语言运行时和 Web 浏览器中。它们共同的方向很明确:短临界区尽量走廉价的快速路径,真正发生竞争时再进入阻塞、排队和调度逻辑。
互斥锁真正昂贵的地方
最简单的互斥锁可以抽象成两个状态:未持有和已持有。线程发现锁被占用后,有几种选择:
- 持续自旋,等待持有者释放锁;
- 让出 CPU,稍后再次尝试;
- 进入休眠队列,由持有者释放锁时唤醒。
每种策略都有代价。自旋避免了线程睡眠和唤醒的调度开销,但如果临界区很长,等待线程会浪费 CPU。直接休眠节省 CPU,却可能引入上下文切换、内核队列操作和缓存扰动。
更复杂的情况是优先级反转:高优先级线程等待低优先级线程持有的锁,而中优先级线程不断占用 CPU,导致低优先级线程迟迟没有机会运行并释放锁。此时,锁不只是一个同步原语,也参与了调度决策。
Turnstile 的核心思路
Turnstile 可以理解为附着在竞争锁上的等待结构。当线程无法立即获得锁时,它不会简单地在全局等待队列中排队,而是进入与该锁相关的队列。这个结构可以记录等待者,并在唤醒过程中配合优先级继承等策略。
这带来几个重要效果:
- 无竞争路径保持轻量:没有等待者时,获取和释放锁不需要维护复杂的阻塞对象。
- 竞争者集中管理:真正等待的线程围绕同一把锁组织起来,减少全局管理成本。
- 调度信息可以随锁传播:当高优先级线程等待锁时,持有者可以暂时获得更高运行优先级,降低优先级反转风险。
- 资源按需出现:只有发生阻塞时才创建或关联更重的等待结构,这与许多现代运行时的设计方向一致。
这里最值得借鉴的不是某个具体数据结构,而是“快速路径加慢速路径”的分层方式。系统不会为每一次加锁都支付阻塞管理成本,而是把复杂性留给真正发生竞争的场景。
这套思想如何进入浏览器和语言运行时
现代 Web 浏览器同时运行 JavaScript、布局、绘制、网络、磁盘和 GPU 相关任务。它们往往有大量短小的共享状态访问,例如引用计数、任务队列、缓存元数据和对象生命周期管理。如果所有操作都直接使用重量级内核锁,锁竞争和线程切换会很快成为瓶颈。
因此,浏览器和运行时通常会组合使用多种技术:
- 原子操作处理简单状态转换;
- 短时间自旋应对即将释放的锁;
- 条件变量或 futex 类机制处理长时间等待;
- 每线程或每核心缓存减少共享写入;
- 任务队列和消息传递降低共享内存的范围;
- 优先级或公平性策略控制等待者的调度顺序。
语言运行时也有类似取舍。垃圾收集器需要在多个线程之间协调堆、标记队列和暂停状态;异步运行时需要管理就绪任务;JIT 编译器需要保护代码缓存与元数据。好的实现通常不会把所有工作都交给一把全局互斥锁,而是把锁拆小,并为高频的无竞争场景设计专门路径。
这并不意味着“锁越少越好”。锁的数量增加后,死锁、锁顺序、内存可见性和调试难度都会上升。Turnstile 影响现代设计的地方,更多在于它提醒工程师:等待管理本身需要被设计,而不是作为互斥锁的附属细节被忽略。
一个可运行的简化等待模型
下面的 Python 示例不是 Solaris Turnstile 的实现,而是一个可以直接运行的教学模型。它把等待者挂在同一个锁对象上,并在锁释放时优先唤醒优先级较高的线程。示例使用 Condition 管理阻塞和唤醒,展示了“无竞争快速获取、发生竞争后排队”的基本结构。
将代码保存为 turnstile_demo.py,使用 Python 3 运行:
import threading
import time
class PriorityTurnstileLock:
def __init__(self):
self._condition = threading.Condition()
self._owner = None
self._waiters = []
self._sequence = 0
def acquire(self, priority):
current = threading.current_thread().name
with self._condition:
if self._owner is None:
self._owner = current
return
self._sequence += 1
waiter = [priority, self._sequence, current]
self._waiters.append(waiter)
while self._owner is not None or self._waiters[0] is not waiter:
self._condition.wait()
self._waiters.remove(waiter)
self._owner = current
def release(self):
current = threading.current_thread().name
with self._condition:
if self._owner != current:
raise RuntimeError('only the owner can release the lock')
self._owner = None
self._condition.notify_all()
lock = PriorityTurnstileLock()
def worker(priority, delay):
time.sleep(delay)
name = threading.current_thread().name
print(f'{name}: waiting, priority={priority}')
lock.acquire(priority)
try:
print(f'{name}: entered critical section')
time.sleep(0.2)
finally:
print(f'{name}: leaving critical section')
lock.release()
# Higher numeric value means higher priority in this teaching example.
threads = [
threading.Thread(target=worker, name='low', args=(1, 0.0)),
threading.Thread(target=worker, name='high', args=(10, 0.05)),
threading.Thread(target=worker, name='medium', args=(5, 0.06)),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
运行命令:
python3 turnstile_demo.py
这个模型有意简化了真实内核的许多细节。它没有实现优先级继承,也没有处理超时、取消、线程退出或内存序;notify_all() 还会让多个等待线程被唤醒后再次竞争。生产代码应使用目标平台提供的同步原语,而不是照搬这个示例。
不过,模型清楚地展示了几个可迁移的设计判断:
- 没有竞争者时,获取锁只需要检查所有者状态;
- 发生竞争后,等待者需要进入与锁关联的结构;
- 唤醒策略可以结合优先级、公平性和等待时间;
- 复杂的调度逻辑应限制在竞争路径上。
落地时需要保留的边界
把 Turnstile 的思想应用到业务系统或运行时设计时,可以从以下检查项开始:
- 测量锁的持有时间、等待时间和竞争比例,不要只看吞吐量;
- 确认临界区是否真的需要共享锁,优先考虑不可变数据、分片和消息传递;
- 对很短且确定会很快结束的临界区评估自旋,对不可预测的等待使用阻塞机制;
- 明确锁的公平性和优先级策略,避免高优先级请求长期饥饿;
- 为取消、超时、线程退出和异常路径设计释放逻辑;
- 使用运行时和操作系统已有的原语,除非确实需要自定义同步协议。
Solaris 的 Turnstile 留下的启发不是“所有系统都应该使用同一种锁”,而是要把同步看成一条完整路径:快速获取、竞争检测、等待排队、唤醒和调度。浏览器与语言运行时之所以能在大量并发任务下保持较低开销,正是因为它们把这些路径拆开,并让常见情况尽可能简单。