版权所有 © 2008-2011 Tim Blechmann
根据 Boost 软件许可协议 1.0 版分发。(请参阅随附文件 LICENSE_1_0.txt 或在 https://boost.ac.cn/LICENSE_1_0.txt 复制副本)
目录
术语 非阻塞 指的是并发数据结构,它们不使用传统的同步原语(如锁)来确保线程安全。Maurice Herlihy 和 Nir Shavit(比较 《多处理器编程的艺术》)区分了 3 种类型的非阻塞数据结构,每种结构都具有不同的属性
某些数据结构只有在特定限制下使用时才能以无锁方式实现。《boost.lockfree
》实现的相关方面是生产者和消费者线程的数量。单生产者 (sp) 或 多生产者 (mp) 表示只允许单个线程或多个并发线程向数据结构添加数据。单消费者 (sc) 或 多消费者 (mc) 表示从数据结构中移除数据的等效情况。
非阻塞数据结构不依赖于锁和互斥锁来确保线程安全。同步完全在用户空间中完成,无需与操作系统直接交互 [7]。这意味着它们不易受到优先级反转等问题的影响(低优先级线程需要等待高优先级线程)。
非阻塞数据结构不依赖于锁,而是需要 原子操作(在没有中断的情况下执行的特定 CPU 指令)。这意味着任何线程要么看到操作之前的状态,要么看到操作之后的状态,但无法观察到中间状态。并非所有硬件都支持相同的原子指令集。如果硬件中没有原子指令,则可以使用锁在软件中模拟。然而,这具有明显的缺点,即失去无锁属性。
在讨论非阻塞数据结构的性能时,必须区分 均摊 成本和 最坏情况 成本。“无锁”和“无等待”的定义仅提及操作的上限。因此,无锁数据结构不一定是每个用例的最佳选择。为了最大化应用程序的吞吐量,应该考虑高性能并发数据结构 [8]。
为了优化系统的延迟或避免优先级反转(这在实时应用程序中可能是必要的),无锁数据结构将是更好的选择。一般来说,我们建议考虑是否需要无锁数据结构,或者并发数据结构是否足够。在任何情况下,我们都建议针对特定的工作负载对不同的数据结构进行基准测试。
除了锁和互斥锁(我们在《boost.lockfree
》中无论如何都不使用它们)之外,还有三个其他方面可能会违反无锁性
某些架构本身并不在硬件中提供必要的原子操作。如果不是这种情况,则使用自旋锁在软件中模拟它们,而自旋锁本身是阻塞的。
从操作系统分配内存不是无锁的。这使得实现真正的动态大小的非阻塞数据结构成为不可能。《boost.lockfree
》的基于节点的数据结构使用内存池来分配内部节点。如果此内存池耗尽,则必须从操作系统分配新节点的内存。但是,《boost.lockfree
》的所有数据结构都可以配置为避免内存分配(相反,特定的调用将失败)。这对于需要无锁内存分配的实时系统尤其有用。
C++ 异常处理不对其实时行为做出任何保证。因此,我们不鼓励在无锁代码中使用异常和异常处理。
boost.lockfree
实现了四种无锁数据结构
boost::lockfree::queue
无锁多生产者/多消费者队列
boost::lockfree::stack
无锁多生产者/多消费者堆栈
boost::lockfree::spsc_queue
无等待单生产者/单消费者队列(通常称为环形缓冲区)
boost::lockfree::spsc_value
无等待单生产者/单消费者值(通常称为三重缓冲区)
数据结构可以使用 Boost.Parameter 风格的模板进行配置
boost::lockfree::fixed_sized
将数据结构配置为 固定大小。内部节点存储在数组内部,并通过数组索引寻址。这限制了队列的可能大小为索引类型可以寻址的元素数量(通常为 2**16-2),但在缺少双倍宽度比较和交换指令的平台上,这是实现无锁性的最佳方法。
boost::lockfree::capacity
在编译时设置数据结构的 容量。这意味着数据结构是固定大小的。
boost::lockfree::allocator
定义分配器。
boost::lockfree::allow_multiple_reads
配置 boost::lockfree::spsc_value
以允许多次读取内容。