从 Solaris Turnstile 到现代浏览器与语言运行时的轻量锁设计

2026-09-15 26 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

很多今天看似理所当然的系统设计,都能在 Solaris 里找到早期而成熟的答案:Slab Allocator 让内核对象分配更高效,OpenZFS 把存储校验与管理能力推向新高度,DTrace 则改变了生产环境的可观测性思路。Turnstile 解决的是另一个经常被低估的问题:线程在竞争互斥锁时,怎样阻塞、唤醒并管理优先级,才能避免锁本身成为系统性能的瓶颈。

这类设计后来以不同形式出现在现代操作系统、编程语言运行时和 Web 浏览器中。它们共同的方向很明确:短临界区尽量走廉价的快速路径,真正发生竞争时再进入阻塞、排队和调度逻辑。

互斥锁真正昂贵的地方

最简单的互斥锁可以抽象成两个状态:未持有和已持有。线程发现锁被占用后,有几种选择:

  • 持续自旋,等待持有者释放锁;
  • 让出 CPU,稍后再次尝试;
  • 进入休眠队列,由持有者释放锁时唤醒。

每种策略都有代价。自旋避免了线程睡眠和唤醒的调度开销,但如果临界区很长,等待线程会浪费 CPU。直接休眠节省 CPU,却可能引入上下文切换、内核队列操作和缓存扰动。

更复杂的情况是优先级反转:高优先级线程等待低优先级线程持有的锁,而中优先级线程不断占用 CPU,导致低优先级线程迟迟没有机会运行并释放锁。此时,锁不只是一个同步原语,也参与了调度决策。

Turnstile 的核心思路

Turnstile 可以理解为附着在竞争锁上的等待结构。当线程无法立即获得锁时,它不会简单地在全局等待队列中排队,而是进入与该锁相关的队列。这个结构可以记录等待者,并在唤醒过程中配合优先级继承等策略。

这带来几个重要效果:

  1. 无竞争路径保持轻量:没有等待者时,获取和释放锁不需要维护复杂的阻塞对象。
  2. 竞争者集中管理:真正等待的线程围绕同一把锁组织起来,减少全局管理成本。
  3. 调度信息可以随锁传播:当高优先级线程等待锁时,持有者可以暂时获得更高运行优先级,降低优先级反转风险。
  4. 资源按需出现:只有发生阻塞时才创建或关联更重的等待结构,这与许多现代运行时的设计方向一致。

这里最值得借鉴的不是某个具体数据结构,而是“快速路径加慢速路径”的分层方式。系统不会为每一次加锁都支付阻塞管理成本,而是把复杂性留给真正发生竞争的场景。

这套思想如何进入浏览器和语言运行时

现代 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 留下的启发不是“所有系统都应该使用同一种锁”,而是要把同步看成一条完整路径:快速获取、竞争检测、等待排队、唤醒和调度。浏览器与语言运行时之所以能在大量并发任务下保持较低开销,正是因为它们把这些路径拆开,并让常见情况尽可能简单。


相关推荐