← Back to Blog
EN中文

票据锁:给等待一个公平的号码

在多线程并发的世界里,“公平”往往是一个被奢侈对待的属性。

大多数常见的互斥锁(Mutex)或自旋锁(Spinlock)并不保证公平性。当锁被释放时,操作系统或硬件调度器决定哪个等待线程胜出,这通常取决于谁运气好(刚好在 CPU 上)或者谁反应快。这种非确定性可能导致“线程饥饿”——某个倒霉的线程可能一直抢不到锁,即便它已经等了很久。

票据锁(Ticket Lock)是解决这个问题的一种优雅方案。它的灵感来源于我们在银行或办事大厅见过的排队叫号系统。

原理:取号与叫号

Ticket Lock 的核心逻辑非常直观,它维护两个原子计数器:

  1. 排队号(Ticket/In):表示下一个可领取的号码。
  2. 服务号(Service/Out):表示当前持有锁(正在被服务)的号码。

当一个线程想要获取锁时,它执行 fetch_add 原子操作拿到一个排队号(Ticket),然后检查这个号码是否等于当前的服务号(Service)。如果不等,它就自旋等待;如果相等,说明轮到它了。

当线程释放锁时,它只需将服务号加 1,唤醒持有下一个号码的线程。

这种机制严格保证了 FIFO(先进先出) 的顺序。先来的线程拿到更小的号码,必然先获得锁。

代码实现 (C++20)

我们可以使用 C++20 的 std::atomic 来演示这一机制。为了避免原始代码的依赖,这里展示一个纯净的现代 C++ 实现。

#include <atomic>
#include <thread>

class TicketLock {
    // 排队号:下一个进入等待的线程领取的号码
    std::atomic<size_t> next_ticket_{0};
    // 服务号:当前允许进入临界区的号码
    std::atomic<size_t> now_serving_{0};

public:
    void lock() {
        // 1. 原子地获取一个号码,并将排队号加1
        // memory_order_relaxed 足够,因为 fetch_add 本身保证原子性,
        // 且后续的加载会有同步屏障
        size_t my_ticket = next_ticket_.fetch_add(1, std::memory_order_relaxed);

        // 2. 自旋等待,直到叫到我的号
        while (now_serving_.load(std::memory_order_acquire) != my_ticket) {
            // 提示 CPU 我们在自旋,避免过度消耗流水线资源
            #if defined(__x86_64__) || defined(_M_X64)
                __builtin_ia32_pause();
            #elif defined(__aarch64__)
                asm volatile("yield");
            #endif
        }
    }

    void unlock() {
        // 3. 叫下一个号
        // 使用 release 语义,确保临界区的写入对下一个线程可见
        size_t current = now_serving_.load(std::memory_order_relaxed);
        now_serving_.store(current + 1, std::memory_order_release);
    }
};

优势与代价

Ticket Lock 的美感在于简洁。它没有复杂的等待队列结构,仅仅依靠两个整数就实现了强公平性。

优势

  • 确定性:彻底消除了线程饥饿。
  • 低开销:在无竞争或低竞争场景下,它只需要一次原子加法和一次比较,非常高效。

代价

然而,凡事皆有代价。Ticket Lock 在高竞争场景下存在著名的可扩展性问题

由于所有等待的线程都在自旋检查同一个变量 now_serving_,这个变量所在的缓存行(Cache Line)会在多个 CPU 核心之间频繁颠簸(Bouncing)。每当持有锁的线程更新 now_serving_ 时,所有等待线程的缓存行都会失效,引发一次总线风暴,尽管最终只有一个线程能成功获取锁。

为了解决这个问题,更复杂的锁(如 MCS Lock 或 CLH Lock)将等待状态分散到每个线程独立的节点上,但这又是另一个故事了。

对于中等程度的竞争,或者当你必须保证处理顺序时,Ticket Lock 是一个极佳的工具。它提醒我们,有时候最简单的排队论原理,就是最高效的计算机科学。