c++oding="utf-8" ?>
不能直接用 std::deque 做非阻塞双端队列,因其操作非 lock-free,存在内存重分配、迭代器失效、malloc 全局锁及缓存不友好等问题;单线程高性能场景应选用 2 的幂容量的环形缓冲(std::array + 位掩码索引),避免动态分配与分支,确保确定性延迟。

为什么不能直接用 std::deque 做非阻塞双端队列
因为 std::deque 的所有操作都不是无锁(lock-free)的,哪怕只在单线程中使用,它内部仍可能触发内存重分配、迭代器失效或异常安全路径中的锁(比如某些 libc++ 实现里 push_front 会间接调用 malloc,而 malloc 在某些环境下有轻量级全局锁)。更重要的是:非阻塞(lock-free)是并发语义,单线程下谈“非阻塞”本质是想表达「零系统调用、零分支预测失败、缓存友好、确定性延迟」——std::deque 的分段式结构导致指针跳转多,prefetch 效果差,实测在高频 push/pop 场景下比紧凑环形缓冲慢 2–3 倍。
用 std::array + 环形索引实现固定容量双端队列
这是单线程高性能场景最稳的选择:避免动态分配、消除分支、全路径内联。关键不是“通用”,而是“你知道容量上限”。
-
front和back用两个size_t索引,不取模,改用位掩码(要求容量为 2 的幂):index & (capacity - 1) - 判空用
size == 0,判满用size == capacity,不依赖front == back(那样要浪费一个槽) - 所有读写都用
std::atomic<t></t>?不需要——单线程下用普通数组 +restrict提示更高效;但若未来要扩展为无锁多生产者,才需对头尾索引做std::atomic<size_t></size_t>+memory_order_relaxed - 移动语义必须显式支持:入队用
std::move(elem),出队后对原位置调用std::destroy_at(&arr[i])(C++17 起),避免析构函数在未构造对象上调用
示例核心片段:
template <typename t size_t n>
class RingDeque {
static_assert((N & (N-1)) == 0, "Capacity must be power of 2");
std::array<t n> arr_;
size_t front_ = 0;
size_t back_ = 0;
size_t size_ = 0;
<p>public:
void push<em>back(T&& x) {
assert(size</em> != N);
new (&arr<em>[back</em>]) T(std::move(x));
back<em> = (back</em> + 1) & (N - 1);
++size_;
}
T pop<em>front() {
assert(size</em> != 0);
T ret(std::move(arr<em>[front</em>]));
arr<em>[front</em>].~T();
front<em> = (front</em> + 1) & (N - 1);
--size_;
return ret;
}
};</p></t></typename>
当容量不可预估时:用两级环形缓冲(segmented ring)
固定容量太死板?又不想退化成 std::deque。可行折中是「固定大小的 segment + 动态 segment 链表」,但链表跳转伤性能。更优解是:分配一大块连续内存(如 64KB),按固定大小(如 256 字节)切分成 slot,每个 slot 存一个指针 + 长度字段,用游标(cursor)管理空闲区。这样仍保持局部性,且扩容只需 mmap 一次大页(Linux)或 VirtualAlloc(Windows),不触发频繁小内存分配。
- 避免用
std::vector<:unique_ptr>></:unique_ptr>:指针间接跳转破坏 cache line,且 unique_ptr 析构开销不可忽略 - 真正需要动态增长时,优先考虑「预分配 + reserve」策略:启动时按业务 P99 任务量分配,运行中只允许向上 resize(用
mmap(MAP_ANONYMOUS)扩展匿名映射),不拷贝旧数据,只更新元信息 - 如果任务对象大小差异极大(从 8 字节到 4KB),改用 slab allocator + freelist,但此时已不属于“双端队列”原语,而是自定义内存池
别忽略任务对象本身的构造/析构成本
再快的队列,如果每次 push 都触发一次 std::string 的堆分配,或者 std::function 的 type-erasure 分配,整体延迟就由队列降级为内存分配器瓶颈。实测显示:在 L3 缓存内完成的队列操作平均 3–5 ns,而一次 small-string 以外的 std::string 构造常超 50 ns。
- 任务对象尽量 POD 或 trivially copyable;非必要不用虚函数、RTTI、异常对象
- 用
std::variant<taska taskb></taska>替代std::function<void></void>,避免堆分配和间接调用 - 若必须延迟执行,把捕获逻辑前置:不在入队时构造闭包,而在调度前用栈上临时对象组装参数,队列只存轻量 handle(如
uint64_t id)
环形缓冲的边界检查、位运算索引、对象生命周期管理——这些细节不难,但错一处就会让“单线程高性能”变成“单线程假象”。真正卡顿往往不出现在队列本身,而出现在你认为“无关紧要”的那个 std::vector::emplace_back 里。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











