545
社区成员
发帖
与我相关
我的任务
分享多线程环境下的锁竞争会带来以下问题:
无锁队列允许多个线程并发访问而不使用互斥锁,通过原子操作(如CAS、Exchange)来保证数据一致性,从而避免上述问题。
memory_order_acquire/release/relaxed)以保证跨线程的可见性和有序性。根据生产者和消费者的数量,无锁队列可分为:
| 类型 | 全称 | 适用场景 |
|---|---|---|
| SPSC | Single Producer Single Consumer | 单一生产者和单一消费者,实现最简单,性能最高 |
| SPMC | Single Producer Multiple Consumers | 单一生产者,多个消费者(较少见) |
| MPSC | Multiple Producers Single Consumer | 多个生产者,单一消费者,如日志收集、任务分发 |
| MPMC | Multiple Producers Multiple Consumers | 多个生产者,多个消费者,通用但实现复杂 |
本文将重点讲解 SPSC环形缓冲区 和 MPSC链表队列 的实现。
环形缓冲区(Ring Buffer)基于固定大小的数组,通过头尾索引实现FIFO。SPSC场景下,生产者和消费者分别操作tail_和head_索引,通过原子操作保证线程安全。
next = (curr + 1) & (cap - 1)。std::aligned_storage预留内存,通过placement new构造/析构对象,支持非POD类型。head_、tail_和缓冲区数据放在不同的缓存行(通常64字节),避免频繁同步导致性能下降。memory_order_acquire/release保证写操作在消费者读之前可见,且读操作能看到最新的写入。#pragma once
#include <atomic>
#include <utility> // std::forward
#include <cstddef> // std::size_t
// SPSC 环形缓冲区,容量必须为2的幂
template <typename T, std::size_t Capacity>
class ringBuffer {
public:
static_assert(Capacity && ((Capacity & (Capacity - 1)) == 0),
"Capacity must be a power of 2");
ringBuffer() : head_(0), tail_(0) {}
~ringBuffer() {
// 析构时,若队列中还有未消费的元素,需要手动调用析构函数
std::size_t head = head_.load(std::memory_order_relaxed);
std::size_t tail = tail_.load(std::memory_order_relaxed);
while (head != tail) {
reinterpret_cast<T*>(&buffer_[head])->~T();
head = (head + 1) & (Capacity - 1);
}
}
// 万能引用,支持左值和右值
template <typename U>
bool Push(U&& value) {
std::size_t tail = tail_.load(std::memory_order_relaxed);
std::size_t next_tail = (tail + 1) & (Capacity - 1);
// 检查队列是否满:下一个tail位置等于head,表示无空位
if (next_tail == head_.load(std::memory_order_acquire)) {
return false; // 队列满
}
// 在缓冲区尾部构造对象(placement new)
new (&buffer_[tail]) T(std::forward<U>(value));
// 更新tail,使用release语义确保之前的构造对其他线程可见
tail_.store(next_tail, std::memory_order_release);
return true;
}
bool Pop(T& value) {
std::size_t head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire)) {
return false; // 队列空
}
// 移动取出元素(使用move优化,避免拷贝)
value = std::move(*reinterpret_cast<T*>(&buffer_[head]));
// 显式析构对象
reinterpret_cast<T*>(&buffer_[head])->~T();
// 更新head,使用release语义保证之前的析构和移动操作对其他线程可见
head_.store((head + 1) & (Capacity - 1), std::memory_order_release);
return true;
}
// 返回当前队列中元素个数(注意:非原子快照,仅用于调试)
std::size_t Size() const {
const std::size_t head = head_.load(std::memory_order_relaxed);
const std::size_t tail = tail_.load(std::memory_order_relaxed);
return tail >= head ? tail - head : Capacity - (head - tail);
}
private:
// 将head_和tail_各自对齐到64字节,避免与buffer_共享缓存行
alignas(64) std::atomic<std::size_t> head_;
alignas(64) std::atomic<std::size_t> tail_;
// 存储元素的原始内存,保证对齐
alignas(64) std::aligned_storage_t<sizeof(T), alignof(T)> buffer_[Capacity];
};
(tail + 1) & (Capacity - 1) 等价于 (tail + 1) % Capacity,但位运算更快。alignas(64)强制将变量放在64字节边界,避免与相邻变量共享缓存行,减少伪共享。tail_.load(std::memory_order_relaxed):生产者读自己的tail,不需要与其他线程同步。head_.load(std::memory_order_acquire):需要看到消费者最新的head值,保证判满的准确性。tail_.store(..., release):使之前对缓冲区的写入在消费者看到新tail时可见。Push接受万能引用,完美转发,避免不必要的拷贝。当有多个生产者但只有一个消费者时,可以采用基于链表的MPSC队列。链表结构可以动态增长,不受固定容量限制,适合任务数不确定的场景。
exchange竞争插入。next置为nullptr,然后将head原子交换为新节点,同时得到旧head,再将旧head的next指向新节点。这样就形成了一个从旧head指向新head的链表(实际上是逆序的,但消费者从tail正向遍历)。tail->next,若不为空则移动tail,取出数据并删除原tail节点。非侵入式队列中,节点包含数据指针和next指针,内存由队列管理。
#ifndef MPSC_QUEUE_NON_INTRUSIVE_H
#define MPSC_QUEUE_NON_INTRUSIVE_H
#include <atomic>
#include <utility>
template<typename T>
class MPSCQueueNonIntrusive {
public:
MPSCQueueNonIntrusive() : _head(new Node()), _tail(_head.load(std::memory_order_relaxed)) {
Node* front = _head.load(std::memory_order_relaxed);
front->Next.store(nullptr, std::memory_order_relaxed);
}
~MPSCQueueNonIntrusive() {
T* output;
while (Dequeue(output))
delete output; // 释放数据对象
Node* front = _head.load(std::memory_order_relaxed);
delete front; // 释放最后一个节点(可能是dummy)
}
// 多生产者入队(wait-free)
void Enqueue(T* input) {
Node* node = new Node(input);
Node* prevHead = _head.exchange(node, std::memory_order_acq_rel);
prevHead->Next.store(node, std::memory_order_release);
}
// 单消费者出队
bool Dequeue(T*& result) {
Node* tail = _tail.load(std::memory_order_relaxed);
Node* next = tail->Next.load(std::memory_order_acquire);
if (!next)
return false; // 队列空
result = next->Data;
_tail.store(next, std::memory_order_release);
delete tail; // 删除原tail节点
return true;
}
private:
struct Node {
Node() = default;
explicit Node(T* data) : Data(data) {
Next.store(nullptr, std::memory_order_relaxed);
}
T* Data;
std::atomic<Node*> Next;
};
std::atomic<Node*> _head;
std::atomic<Node*> _tail;
// 禁止拷贝
MPSCQueueNonIntrusive(const MPSCQueueNonIntrusive&) = delete;
MPSCQueueNonIntrusive& operator=(const MPSCQueueNonIntrusive&) = delete;
};
#endif
说明:
head和tail初始指向它,避免空指针判断。Enqueue:_head.exchange(node, acq_rel)原子地将_head更新为新节点,并返回旧head。然后设置旧head的Next指向新节点。这一步保证了多生产者并发时的正确链接。Dequeue:消费者从tail开始,如果tail->Next存在,则取出数据,更新tail,并删除原tail节点。exchange使用acq_rel:读取旧head需要acquire,写入新head需要release,同时保证对旧head的后续操作可见。Next.store使用release:确保节点完全构造后再让消费者看到。Next.load使用acquire:保证看到生产者设置的Next。侵入式队列要求节点类型T内部包含一个std::atomic<T*>成员作为next指针,队列操作直接利用该成员,无需额外分配节点对象,节省内存,减少缓存缺失。
#ifndef MPSC_QUEUE_INTRUSIVE_H
#define MPSC_QUEUE_INTRUSIVE_H
#include <atomic>
#include <type_traits>
#include <new>
template<typename T, std::atomic<T*> T::* IntrusiveLink>
class MPSCQueueIntrusive {
public:
MPSCQueueIntrusive()
: _dummyPtr(reinterpret_cast<T*>(std::addressof(_dummy))),
_head(_dummyPtr),
_tail(_dummyPtr) {
// 只初始化dummy节点的IntrusiveLink成员(因为T可能不可默认构造)
std::atomic<T*>* dummyNext = new (&(_dummyPtr->*IntrusiveLink)) std::atomic<T*>();
dummyNext->store(nullptr, std::memory_order_relaxed);
}
~MPSCQueueIntrusive() {
T* output;
while (Dequeue(output)) {
delete output; // 注意:这里删除的是数据对象本身,其内存可能由外部管理,需谨慎
}
}
// 入队
void Enqueue(T* input) {
(input->*IntrusiveLink).store(nullptr, std::memory_order_release);
T* prevHead = _head.exchange(input, std::memory_order_acq_rel);
(prevHead->*IntrusiveLink).store(input, std::memory_order_release);
}
// 出队
bool Dequeue(T*& result) {
T* tail = _tail.load(std::memory_order_relaxed);
T* next = (tail->*IntrusiveLink).load(std::memory_order_acquire);
// 如果tail是dummy节点,需要特殊处理:跳过dummy
if (tail == _dummyPtr) {
if (!next) return false; // 队列空
_tail.store(next, std::memory_order_release);
tail = next;
next = (next->*IntrusiveLink).load(std::memory_order_acquire);
}
if (next) {
_tail.store(next, std::memory_order_release);
result = tail;
return true;
}
// 此时tail可能是最后一个节点,需要检查是否有新节点刚入队
T* head = _head.load(std::memory_order_acquire);
if (tail != head) // 如果head已经更新,但tail->Next尚未设置(可能刚执行exchange但未设置Next)
return false; // 让消费者重试?此处简单返回false,实际可自旋或依赖外部重试
// 队列为空,但需要将dummy重新入队以便下次使用(防止tail超过head)
Enqueue(_dummyPtr);
next = (tail->*IntrusiveLink).load(std::memory_order_acquire);
if (next) {
_tail.store(next, std::memory_order_release);
result = tail;
return true;
}
return false;
}
private:
std::aligned_storage_t<sizeof(T), alignof(T)> _dummy;
T* _dummyPtr;
std::atomic<T*> _head;
std::atomic<T*> _tail;
MPSCQueueIntrusive(const MPSCQueueIntrusive&) = delete;
MPSCQueueIntrusive& operator=(const MPSCQueueIntrusive&) = delete;
};
#endif
要点:
_dummy是一个aligned_storage,只用来占位,不构造T对象,仅初始化其IntrusiveLink成员。std::conditional_t可以定义一个统一的MPSCQueue别名,根据是否提供IntrusiveLink自动选择版本。C++11提供了六种内存顺序,理解它们对无锁编程至关重要:
memory_order_relaxed:仅保证原子性,无顺序约束。memory_order_acquire:防止之后的内存读写被重排到该操作之前。memory_order_release:防止之前的内存读写被重排到该操作之后。memory_order_acq_rel:读-修改-写操作同时拥有acquire和release语义。memory_order_seq_cst:顺序一致性,最严格但也最慢。在SPSC中,我们用acquire读对方的索引,用release写自己的索引,保证了队列操作的顺序。在MPSC中,exchange使用acq_rel确保获取旧head的完整可见性。
链式队列需要动态分配节点。频繁的new/delete可能成为性能瓶颈。优化手段包括:
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SPSC环形缓冲区 | 极高性能,无锁且无等待 | 容量固定,不适用于动态增长 | 音频处理、实时数据流、单生产者单消费者管道 |
| MPSC链表 | 支持多生产者,动态大小 | 节点分配开销,出队可能稍慢 | 日志聚合、任务队列、事件分发 |
实际选择时,需根据生产者/消费者数量、对延迟和吞吐的要求、内存限制等因素权衡。
无锁队列是高性能并发编程的重要组件。本文从基础概念出发,详细剖析了SPSC环形缓冲区和MPSC链表队列的实现,并讨论了内存序、内存管理等核心挑战。理解这些设计思想后,读者可以根据实际需求实现或选用合适的无锁队列。
https://blog.csdn.net/qq_57951250/article/details/159044528?spm=1011.2124.3001.6209