环形缓冲区为什么是lock-free的?

环形缓冲区的 lock-free 原理是什么?

你的困惑非常典型,而且问到了点子上。

先直接回答:维基那段描述的"flag 方案"和"lock-free"是两回事,不要混为一谈。 维基那段说的是"如何判断 buffer 满还是空"的问题,不是 lock-free 本身的实现方案。

注意:文末有25个C++硬核项目推荐,可写简历。校招/社招简历都可以写。

一、先把问题拆开

环形缓冲区有两个独立的问题:

问题一:如何判断 buffer 是满还是空?

这是一个纯逻辑问题。当 head == tail 时,到底是满还是空?常见解法有三种:

  • 浪费一个槽位(buffer 大小 N,实际只用 N-1 个槽)
  • 额外维护一个 size 计数器
  • 维基说的 flag 标志位

这三种方案都是在回答"怎么区分满和空",跟 lock-free 没有直接关系

问题二:多线程并发读写时,如何保证正确性?

这才是 lock-free 要解决的问题。

二、lock-free 的真正前提:SPSC

说"环形缓冲区是 lock-free 的",这个说法本身是有前提条件的,这个前提叫做:

SPSC — Single Producer Single Consumer(单生产者单消费者)

在 SPSC 场景下,环形缓冲区可以做到真正的 lock-free,而且不需要任何 CAS 原子操作,只需要普通的 std::atomic load/store 就够了。

为什么?核心洞察在这里:

Producer 只写 write_pos(写指针)
Consumer 只写 read_pos(读指针)

写指针只被一个线程修改,读指针只被另一个线程修改。

这意味着:

  • Producer 读 read_pos 判断是否满 → 只读,不写
  • Consumer 读 write_pos 判断是否空 → 只读,不写

没有两个线程同时写同一个变量,自然不需要锁,也不需要 CAS。

三、具体实现

template<typename T, size_t N>
class SPSCQueue {
    T buffer[N];
    std::atomic<size_t> write_pos{0};
    std::atomic<size_t> read_pos{0};

public:
    // 生产者调用(只有一个线程调用)
    bool push(const T& val) {
        size_t cur_write = write_pos.load(std::memory_order_relaxed);
        size_t next_write = (cur_write + 1) % N;
        
        // 读 read_pos 判断是否满
        if (next_write == read_pos.load(std::memory_order_acquire)) {
            return false; // 满了
        }
        
        buffer[cur_write] = val;
        
        // 发布写指针,让 Consumer 可见
        write_pos.store(next_write, std::memory_order_release);
        return true;
    }

    // 消费者调用(只有一个线程调用)
    bool pop(T& val) {
        size_t cur_read = read_pos.load(std::memory_order_relaxed);
        
        // 读 write_pos 判断是否空
        if (cur_read == write_pos.load(std::memory_order_acquire)) {
            return false; // 空了
        }
        
        val = buffer[cur_read];
        
        // 发布读指针,让 Producer 可见
        read_pos.store((cur_read + 1) % N, std::memory_order_release);
        return true;
    }
};

注意 memory order 的选择:

  • relaxed:读自己那侧的指针,不需要同步
  • acquire:读对方的指针,需要看到对方之前的所有写操作
  • release:更新自己的指针,发布自己之前的所有写操作

这里没有任何 mutex,没有任何 CAS,只有普通的原子 load/store。这就是 SPSC 环形队列 lock-free 的完整实现。

四、为什么维基那段说"需要同步"?

再回头看你引用的那段:

Read and write operations must share the flag, so it will probably require synchronization in multi-threaded situation.

维基说的 flag 方案问题在于:flag 需要被读写两侧共同访问和修改

每次 push 之后要写 flag = "last was write",每次 pop 之后要写 flag = "last was read"。

这就意味着 Producer 和 Consumer 都要写同一个变量(flag),破坏了 SPSC 的关键前提——每个变量只被一个线程写。所以这个方案反而引入了竞争,需要同步。

这是一个典型的"为了解决一个问题,引入了更大的问题"的案例。

正确的区分满/空方法,在 SPSC 场景下,应该用"浪费一个槽位"的方案:buffer 大小 N,实际只存 N-1 个元素,(write+1)%N == read 表示满,write == read 表示空。简洁,无共享状态,天然 lock-free。

五、MPMC 的情况

如果是多生产者多消费者(MPMC),情况就复杂很多。

这时候 write_pos 会被多个 Producer 同时修改,read_pos 会被多个 Consumer 同时修改,不能用简单的 load/store,需要用 CAS(Compare-And-Swap):

// MPMC push 的核心逻辑(简化)
size_t cur = write_pos.load(std::memory_order_relaxed);
while (!write_pos.compare_exchange_weak(cur, cur + 1, ...)) {
    // CAS 失败,重试
}

这仍然是 lock-free 的(没有 mutex),但用了 CAS 原子操作,开销比 SPSC 大得多,而且实现复杂很多(需要处理槽位状态、ABA 问题等)。

六、总结一下

场景实现方式是否 lock-free
SPSCatomic load/store + 内存序✅ 是,且开销极低
MPSC/SPMCCAS✅ 是,开销较高
MPMCCAS + 槽位状态机✅ 是,实现复杂
带 flag 判满方案需要同步 flag❌ 不再 lock-free

lock-free 的本质不是"不用任何同步原语",而是"不用互斥锁,系统整体总有线程在推进"。 SPSC 环形队列之所以优雅,是因为它利用了生产者和消费者天然的职责分离,让每个指针只被一个线程修改,从而连 CAS 都省掉了。


如果你对无锁数据结构感兴趣,想从理论到真正能跑的代码都搞通,可以关注我的两个项目课程:

SPSC LightningQueue(无锁队列单生产单消费版,100元)和 MPMC ThunderQueue(多生产多消费版,299元)——从内存模型、happens-before 语义讲起,到 cache line 对齐、false sharing 优化,再到完整实现和压测验证,每个设计决策都说清楚"为什么"。

这两个是我 25 个 C/C++ 硬核项目课程里的一部分。除了无锁队列,还有线程池、内存池、协程库、高性能网络库、LSM 存储引擎,以及最近刚完成的重头项目——

NginX0(miniNginx):参考 Nginx 1.26.2 源码,从空文件夹出发 29 天增量实现,覆盖内存池、epoll 事件循环、Phase Engine(11个阶段)、过滤器链、upstream 反代框架、Slab 共享内存 + 跨 Worker 限流、Rewrite 字节码虚拟机……核心代码 2.2 万行,单 Worker 静态文件 QPS 压测达到官方 Nginx 的 98%。这不是玩具项目,是真实可用的性能水平,也是能放进简历、在面试里展开讲的那种。

25 个项目累计带了 450+ 同学从 0 到 1 做完,覆盖校招应届生到工作多年的同学。

感兴趣加微信 jkfwdkf,备注「 项目实战 」查看完整目录,打包购买有优惠。

C++项目实战课程海报:

想系统补齐项目经验、丰富简历、提升面试可聊内容,可以点击下面这 篇查看完整介绍:

小康的 C++ 项目实战课程目录|持续更新

对C++项目实战课程感兴趣,欢迎微信搜索 jkfwdkf,备注「 项目实战 」,打包购买有优惠。

最近新开放的 C/C++ 项目实战课程(校招/社招简历都可以写):

一个能让面试官眼前一亮的C/C++项目:从零手写 NginX0 (Mini-Nginx),性能追平官方98%

XRPC:一个能写进简历的 C++ 高性能分布式 RPC 框架,QPS 13万+

2万行代码、36天、16万ops/s:我从零手写了一个Mini-Redis

VortexCache:一个能写进简历的 C++ 高性能分布式缓存服务器,多场景吞吐超越 memcached

16天手写 HTTP 服务器:从 Reactor 到 45万QPS 生产级实现

我把一个完整的C++ B/S 聊天室项目拆成了 16 天,每天增量实现,你跟着做一遍就彻底懂了

手写 C++ LSM存储引擎:MemTable、SSTable、Compaction 全流程实现

SGI STL 源码精华:从内存池到红黑树,我用 C++11 重新实现了一遍

编辑于 2026-06-09 · 著作权归作者所有