具有连续预留的无锁环形缓冲区(2019)

无锁环形缓冲区和相关的“bip 缓冲区”被视为用于单生产者/单消费者通信的高性能数据结构,尤其适用于必须避免动态分配或阻塞的系统(例如 DMA、嵌入式、实时系统)。评论者比较了 LMAX Disruptor 和“魔法”虚拟内存映射环形缓冲区等设计,在优雅性和性能之间权衡其平台特定的 `unsafe` 代码、TLB 开销和设置成本等缺点。讨论的很大一部分集中在原子内存序、缓存行为和正确性保证的细微差别上,强调了实现真正安全、高效的无锁结构有多困难,以及 TLA+ 或 Loom 等工具如何提供帮助。

相关设计与实现

  • 多位评论者提到了类似的 SPSC/MPMC 环形缓冲区,以及 Java、C++、Rust、Crystal、Ruby、Go 和 C/C++ 库中的 LMAX Disruptor 风格队列。
  • 双分区(bip)缓冲区因其对可变大小负载的强大能力而受到特别称赞,但使用得并不多。

虚拟内存“魔法环形缓冲区”技巧

  • 有几位提到将缓冲区在虚拟内存中连续映射两次(“魔法环形缓冲区”),以避免处理回绕,这种做法用于 GNU Radio 和 BPF。
  • 优点:简化可变大小消息的处理;非常易用。
  • 缺点:需要操作系统特定的 mmap 风格 API 和 unsafe 代码;设置/拆除成本高于普通分配;对于大量小缓冲区来说会带来较多的页和 TLB 开销。
  • 它只是在虚拟内存中连续,因此不适合需要物理内存连续的 DMA;IOMMU 理论上可以提供帮助,但在低端微控制器上通常不存在。

Rust、unsafe 与可移植性

  • 关于可以接受多少 unsafe 存在争论:有人说任何生产级环形缓冲区都必须使用 unsafe;也有人强调小而受控的 unsafe 与大规模、平台特定的 VM 代码之间的区别。
  • 镜像分配显然是平台特定的,并且每个操作系统都需要单独的代码路径。

正确性与验证

  • 目前尚不清楚是否有正式规格或证明;有人建议用 TLA+ 做模型检查。
  • 一位评论者质疑了某段特定的 watermark 更新代码,并提出一种基于所有权的非原子 watermark 方案。
  • 其他人表示,他们通过动态分析以及在 x86 和 ARM 上的 CI 来捕获排序错误。

有界 vs 无界 / 广播日志

  • 有界广播日志被认为很直接;而让它们变成无界且高效则很困难。
  • 有人建议使用由多个有界日志组成的链表,再加上索引间接层,但这通常会重新引入锁。
  • 多位评论者认为,锁通常已经“足够好”,而且在某些架构上有时甚至比复杂的无锁设计更快。

原子操作、内存序与“无锁”

  • 有人澄清,“无锁”是一个术语:算法使用原子操作(而原子操作会使用硬件锁),但它们保证前进性并避免阻塞其他线程。
  • 强烈建议理解 acquire/release 与 SeqCst 之间的区别,而不是默认使用 SeqCst;后续引用还强调了更好的内存序学习材料。
  • 讨论深入到诸如 ABA、hazard pointers、不对称屏障,以及在弱内存模型下进行推理的困难等细微问题。