tbb::task_group与std::barrier间偶发死锁问题求助
问题场景
我在应用中使用Intel OneAPI TBB实现并行化,通过task_arena将工作隔离在NUMA节点上,每个NUMA节点的并行任务由task_group管理。部分任务需要与其他NUMA节点同步,尝试用std::barrier实现,但测试时会偶发死锁,有时又正常。
复现代码:
#include <oneapi/tbb/info.h> #include <oneapi/tbb/task_arena.h> #include <oneapi/tbb/task_group.h> #include <barrier> using namespace oneapi; int main(int argc, char* argv[]) { std::vector<tbb::numa_node_id> numa_indexes = tbb::info::numa_nodes(); std::vector<tbb::task_arena> arenas(numa_indexes.size()); std::vector<tbb::task_group> task_groups(numa_indexes.size()); std::barrier barrier(numa_indexes.size()); for (unsigned j = 0; j < numa_indexes.size(); j++) { arenas[j].initialize(tbb::task_arena::constraints(numa_indexes[j])); } for (int i = 0; i < 10000; ++i) { for (unsigned j = 0; j < numa_indexes.size(); j++) { arenas[j].execute([&task_groups, &barrier, j]() { task_groups[j].run([&barrier]() { barrier.arrive_and_wait(); }); }); } for (unsigned j = 0; j < numa_indexes.size(); j++) { arenas[j].execute([&task_groups, j]() { task_groups[j].wait(); }); } } }
注:若直接在arenas[j].execute()中调用barrier.arrive_and_wait()(不使用task_group)则运行正常,但这样无法等待特定任务完成。当前使用版本为oneTBB 2021.9.0、gcc 13.1.0。
死锁核心原因
死锁源于task_group的任务调度逻辑与std::barrier的同步机制,在TBB arena的NUMA隔离约束下形成了循环等待:
task_group任务的延迟执行特性
调用task_groups[j].run()时,任务不会立即在当前arena线程上执行,而是被放入该arena的任务队列,等待arena线程池中的空闲线程拾取。而执行arenas[j].execute()的线程在提交任务后,会直接退出execute()上下文,回到arena线程池。wait()操作的阻塞风险
后续调用arenas[j].execute([&task_groups, j]() { task_groups[j].wait(); })时,执行这个lambda的arena线程会被wait()阻塞,直到task_group中的所有任务完成。如果此时该arena的线程池中没有其他空闲线程,那么之前提交到队列中的barrier.arrive_and_wait()任务就永远无法被执行。循环等待链的形成
假设存在两个NUMA节点A和B:- 节点A的arena线程提交task_group任务后,立即进入
wait()阻塞,占用了该arena的唯一可用线程; - 节点A的
barrier.arrive_and_wait()任务因无空闲线程无法执行,无法完成barrier的arrive操作; - 节点B的任务已经到达barrier,但因A的任务未到达,会一直阻塞在
barrier.arrive_and_wait(); - 最终节点A的
wait()在等任务完成,任务在等barrier放行;节点B的任务在等A的任务到达barrier,形成循环等待,触发死锁。
- 节点A的arena线程提交task_group任务后,立即进入
无task_group时正常的原因
直接在arenas[j].execute()中调用barrier.arrive_and_wait()时,执行操作的就是当前arena的线程,会直接完成barrier的arrive动作,不存在任务排队、线程被阻塞的矛盾,因此不会死锁。
偶发死锁的随机性
死锁并非每次出现,是因为arena线程池的调度存在随机性:如果提交task_group任务后,arena还有其他空闲线程可以拾取并执行barrier.arrive_and_wait(),流程就能正常完成;但如果所有线程都被wait()阻塞,就会触发死锁。
内容的提问来源于stack exchange,提问作者Daniel

