std::deque 不适合工作窃取队列,因其迭代器易失效、内存不连续且 pop_back/push_back 在多线程下需内部锁;工业级方案是用 std::vector + 两个 cache-line 对齐的 std::atomic 字段(m_top/m_bottom)实现无锁双端队列。

std::deque 不能直接用作窃取队列
很多初学者会直接拿 std::deque 当作每个线程的本地任务队列,以为它支持双端操作就天然适合工作窃取。实际不是:它的迭代器可能失效、内存不连续、pop_back() 和 push_back() 在多线程下仍需内部锁(尤其在 libstdc++/MSVC 实现中),一旦多个线程并发调用,性能断崖式下跌。
真正工业级做法是自己模拟双端队列——用 std::vector<task></task> + 两个 std::atomic<size_t></size_t> 字段:m_top(本地执行栈顶)和 m_bottom(最新插入位置)。所有本地 push/pop 只改 m_bottom 或 m_top,偷取方只读 m_bottom 并用 CAS 尝试递减,完全无锁。
- 别用
std::stack默认适配器(底层是std::deque),它会在偷取时隐式加锁 -
m_top和m_bottom必须对齐 cache line,否则伪共享会让原子操作变慢数倍 - 每次 push 后要
atomic_thread_fence(memory_order_release),确保任务数据对偷取方可见
steal() 必须是 try-steal,失败立即放弃
偷取不是“拿走一个任务”,而是“尝试从别人队列底部原子地挪走一个任务”。如果失败(比如被本地线程同时 pop 掉),必须立刻返回 false,而不是重试或自旋——否则空转耗 CPU,反而拖垮整体吞吐。
典型错误是写成 while 循环反复 try_steal,或者在失败后 sleep(1ms)——这既破坏了零阻塞目标,又让线程调度器误判为“活跃等待”,影响其他线程调度。
立即学习“C++免费学习笔记(深入)”;
- 偷取函数签名应为
bool try_steal(task*& out),返回 false 表示无任务可偷或竞争失败 - 偷取失败后应 fallback 到全局 MPSC 队列检查,或调用
std::this_thread::yield()让出时间片 - 千万别在 steal 路径里加 mutex、condition_variable 或任何阻塞原语
优先级感知的 steal 怎么做
标准 work-stealing(如 TBB)默认 FIFO 窃取,但业务任务常带优先级。若从别人队尾偷到一堆低优任务,等于白忙——关键不是排序整个队列,而是让偷取方快速判断“这个队列值不值得偷”。
实操方案是每个 worker 维护一个 std::atomic<int64_t> min_priority_hint</int64_t>,每次 push 到队尾时用 fetch_min() 更新(x86 上需 lock cmpxchg 模拟)。偷取前先批量读所有空闲 worker 的 hint,挑最高值的那个发起 steal;若为 INT64_MIN(空队列),跳过。
- hint 不必绝对准确,只要能过滤掉明显低优队列即可;更新延迟几微秒不影响正确性
- steal 实际执行时仍要检查每个任务的
priority字段,低于阈值的放回队首(push_front()),避免污染本地 LIFO 顺序 - 不要用跳表或堆维护全局优先级视图——那是重调度用的,不是每毫秒分发路径上的
主线程提交任务该进哪里
所有 submit() 都不该直接塞进某个固定线程的本地队列,否则初始负载就失衡。标准做法是走一个无锁 MPSC(multi-producer, single-consumer)全局队列,由空闲 worker 轮询消费。
这个 MPSC 队列可以是 ring buffer + 原子索引,也可以是 Michael-Scott 无锁链表。关键是它只承担“初始分发”,不参与高频窃取路径——本地队列满时才触发 fallback 到 MPSC,避免热点。
- submit() 返回
std::future时,std::packaged_task必须分配在堆上(避免小对象优化导致 move 失败) - MPSC 队列需有界,否则任务堆积会 OOM;建议配合背压策略,如 submit() 返回 false 或 throw
std::runtime_error("queue full") - 别把 MPSC 和本地队列混用同一套内存池——它们生命周期和访问模式完全不同,混用容易引发 use-after-free
m_bottom 字段,就能让吞吐掉 30%;一个没加 memory_order_acquire 的读,就会让偷取方看到脏任务指针。这些细节不在接口里,全藏在 atomic 操作的 pairing 关系中。


















