文章 · 2026-02-26

符号位里的红绿灯:一种极致的轻量级读写锁实现

在构建高吞吐量的分布式系统时,读多写极少的工作负载是常态。标准的 std::shared_mutexpthread_rwlock_t 是通用工具,但通用往往意味着妥协。当你的系统 99.9% 的时间都在读取数据,而只有 0.1% 的时间需要写入时,每一次获取读锁的开销(哪怕只是几次额外的原子操作或缓存行竞争)累积起来都是惊人的。

某些工业级分布式基础库采用的设计充分利用了整数的符号位(Sign Bit)和 Linux Futex 机制,将读锁的开销压缩到了极限。我们将使用 Zig 语言复现其核心思想,以更清晰地展示其内部逻辑。

核心设计:把锁塞进一个整数

这个锁设计的关键在于:整个锁的状态仅由一个 32 位整数(counter)维持。

一个有符号的 32 位整数(i32),其最高位(第 31 位)是符号位。

设计者巧妙地利用了这一点:

  1. 低 31 位:用于记录当前的活跃读者数量
  2. 最高位(第 31 位):作为写锁等待/持有标记(Write Flag)。

这种布局带来的关键优势是:读者获取锁只需要一条原子指令。

读者的视角:乐观也是一种策略

在绝大多数读写锁实现中,读者进入时需要检查"是否有写者正在等待"。如果有,则通常需要排队,以避免写者饥饿。

在这种设计中,读者采用了极度乐观的策略。

pub fn acquireRead(self: *LightweightRWLock) void {
    while (true) {
        // 1. 无论如何,先加了再说!
        // 这是一个原子加法操作,返回旧值
        const prev = self.counter.fetchAdd(1, .SeqCst);
        
        // 2. 检查结果
        // 如果符号位是 0 (正数),说明没有写者。
        // 我们已经成功把读者计数 +1 了,直接持有锁!
        if (prev & WRITE_BIT == 0) return;

        // 3. 哎呀,有写者(符号位是 1)
        // 我们刚才鲁莽地 +1 了,现在必须撤销这个操作
        _ = self.counter.fetchSub(1, .SeqCst);
        
        // 4. 乖乖去内核态排队,等待写者干完活
        self.waitForWriter();
    }
}

代价:回退机制

这种设计展示了一个尖锐的权衡:

这是一种典型的赌博式设计:赌"写操作"极少发生。一旦赌赢了,吞吐量极高;输了,代价虽有但可控。

写者的视角:霸道地占据符号位

写者的逻辑则更加直接。当写者想要获取锁时,它不关心当前有多少读者,关心的第一件事是:立起"禁止入内"的牌子。

pub fn acquireWrite(self: *LightweightRWLock) void {
    while (true) {
        // 1. 尝试原子地设置最高位 (Write Bit)
        // fetchOr 会将指定位设为 1,并返回旧值
        const prev = self.counter.fetchOr(WRITE_BIT, .SeqCst);
        
        // 2. 检查之前是不是已经有别的写者了?
        if (prev & WRITE_BIT != 0) {
            // 已经有同行在排队,那我去睡会儿
            self.waitForWriter();
            continue; // 醒来后重试
        }

        // 3. 成功抢到了符号位!
        // 现在新的读者进不来了(因为他们看到符号位是 1 会回退)。
        // 但是!旧的读者可能还没读完。
        
        // 4. 等待现存的读者清零
        // 只要低 31 位不是 0,我就死等
        while (self.counter.load(.SeqCst) & ~WRITE_BIT != 0) {
            self.waitForReaders();
        }
        
        // 所有读者都走了,写锁正式归我
        return;
    }
}

写者利用 fetchOr 原子地占据了符号位。这一步瞬间切断了后续读者的流量。然后,写者需要耐心地等待当前的读者计数归零。

内核态的协作:Futex

虽然原子操作很快,但如果锁被占用的时间稍长,忙轮询(Spinning)会极大地浪费 CPU。这就是 Linux Futex (Fast Userspace Mutex) 发挥作用的地方。

在真实实现中,waitForWriterwaitForReaders 会封装 syscall(SYS_futex, ...)

这种用户态原子操作 + 内核态挂起的组合是现代高性能锁的基石。

关键原则

  1. 极度优化热点路径:读是最频繁的操作,被简化为单一原子指令。
  2. 利用整数特性:符号位在并发控制中可以是高效的状态标志。
  3. 接受回退:为了追求极致的常见路径性能,允许在写者竞争时付出回退的代价。

在 Zig 这样能精准控制内存布局和系统调用的语言中,理解并重构这样的底层原语,展现了系统编程深度思考的价值。

© 2026 Yuxu Ge ·