oneTBB task_group 并发任务组完全解析:基于 mold 仓库内置 oneTBB 的源码级使用指南
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
导读
本文以 mold 仓库内置的 oneTBB 规范文档 task_group_cls.rst 为骨架,系统讲解oneapi::tbb::task_group这一并发任务组的完整接口、语义与底层实现。mold 作为一款现代链接器,在其构建体系中以 third-party/tbb 的形式内嵌了 oneTBB 源码,因此本文涉及的类、头文件与测试用例均可在当前仓库中直接检索验证。读完本文,你将掌握 task_group 的动态任务提交、延迟任务(task_handle)、等待、取消与异常处理的全部用法,并能结合源码理解其调度机制,直接用于自己的并行算法设计。
task_group:一组可动态扩展的并发任务
按照规范文档的定义,task_group表示一组任务的并发执行。它有两点关键语义:
- 动态性:组在运行期间可以随时向其中添加新任务(通过
run或defer),不必在创建组时预先确定任务集合; - 参与式等待:调用
task_group::wait()的线程在等待期间可能参与执行与当前 task_group 无关的其他任务,这是 oneTBB 工作窃取(work stealing)调度模型的直接体现,等待线程不会被白白闲置。
该类的正式声明定义在头文件<oneapi/tbb/task_group.h>中,完整接口如下(摘自规范文档原文):
// Defined in header <oneapi/tbb/task_group.h> namespace oneapi { namespace tbb { class task_group { public: task_group(); task_group(task_group_context& context); ~task_group(); template<typename Func> void run(Func&& f); template<typename Func> task_handle defer(Func&& f); void run(task_handle&& h); template<typename Func> task_group_status run_and_wait(const Func& f); task_group_status run_and_wait(task_handle&& h); task_group_status wait(); void cancel(); }; bool is_current_task_group_canceling(); } // namespace tbb } // namespace oneapi在仓库实际的 task_group.h 中,task_group继承自内部基类task_group_base,并通过命名空间注入机制在oneapi::tbb与tbb两个命名空间下同时可见(见 task_group.h 中的inline namespace v1别名导出)。
构造函数与析构约束
| 接口 | 语义 |
|---|---|
task_group() | 构造一个空的任务组。从实现看,该构造函数会以task_group_context::concurrent_wait特质创建内部上下文(见 task_group.h),允许wait()被并发地多次调用 |
task_group(task_group_context& context) | 构造空任务组,并将组内所有任务关联到用户提供的context上。对应实现见 task_group.h,其内部构造一个指向该上下文的代理(proxy)上下文 |
~task_group() | 销毁任务组。要求:销毁前必须先调用wait(),否则析构函数抛出异常 |
析构约束的实现位于基类析构函数 task_group.h:如果析构时组内还有未完成的任务(m_wait_vertex.continue_execution()为真),实现会先尝试cancel()取消剩余任务并等待其收尾,然后:
- 若当前正处于栈展开(stack unwinding)过程中(存在未捕获异常),则静默放行,避免掩盖原始异常;
- 否则抛出
exception_id::missing_wait异常,强制提醒开发者"等待缺失"这一编程错误。
换句话说,规范的"必须 wait"不是一句空话,而是由运行时保证的强约束:忘记调用wait()会在析构时得到明确的异常反馈,而不是静默的内存错误。
任务提交:run 的两种形态
提交可调用对象
template<typename Func> void run(Func&& f);run将一个计算f()的任务加入组内并立即返回,不等待f执行完成。Func类型必须满足 ISO C++ 标准 [function.objects] 一节中 Function Objects 的要求(即可调用、可复制/移动)。
对应实现 task_group.h 的核心是:
template<typename F> void run(F&& f) { d1::spawn(*prepare_task(std::forward<F>(f)), context()); }prepare_task(见 task_group.h)通过 oneTBB 的小对象分配器small_object_allocator构造一个function_task,并把它登记到组的等待树顶点(wait_tree_vertex)上;随后d1::spawn将任务投递到调度器,由工作线程按需窃取执行。
提交延迟任务句柄
void run(task_handle&& h);该重载将task_handle h指向的延迟任务正式排入调度队列。规范明确列出两条不满足即触发未定义行为(undefined behavior)的前提条件:
h不能为空(empty);*this必须是创建h的同一个task_group。
这两条约束在实现中以断言形式落地(见 task_group.h):
void run(d2::task_handle&& h) { __TBB_ASSERT(h != nullptr, "Attempt to schedule empty task_handle"); using acs = d2::task_handle_accessor; __TBB_ASSERT(&acs::ctx_of(h) == &context(), "Attempt to schedule task_handle into different task_group"); task_handle_task* task_ptr = acs::release(h); d1::spawn(*task_ptr, context()); }注意acs::release(h)会把句柄中的任务指针“释放”出来交给调度器,因此h在被run之后即变为空句柄,符合task_handle的移动语义。
延迟任务:defer 与 task_handle
template<typename F> task_handle defer(F&& f);defer创建一个计算f()的延迟任务并返回指向它的task_handle。延迟任务有两个重要特点:
- 不会自动调度:在显式通过
run(task_handle&&)(或run_and_wait(task_handle&&))请求执行之前,任务不会进入调度队列; - 仍然计入组内:延迟任务一旦创建便已加入
task_group,因此wait()会一直等待该task_handle被调度执行或被销毁为止——这保证不会出现"任务被创建后无人认领导致 wait 提前返回"的竞态。
F同样必须满足 Function Objects 要求。
task_handle本身是一个拥有延迟任务对象的移动语义句柄,其规范定义在 task_handle.rst:
class task_handle { public: task_handle(); // 创建空句柄 task_handle(task_handle&& src); // 移动构造,src 变为空 ~task_handle(); // 销毁句柄及关联任务(若存在) task_handle& operator=(task_handle&& src); explicit operator bool() const noexcept; // 是否关联了任务 };并配套提供与nullptr比较的operator==/operator!=非成员函数,用于判断句柄是否为空。在仓库实现 _task_handle.h 中,task_handle本质上是对task_handle_task*的std::unique_ptr包装,因而不可拷贝、只可移动;析构时若句柄仍持有任务,会通过task_handle_task_deleter正确释放任务对象。
defer 的典型使用场景
延迟任务非常适合"先定义工作单元、稍后按需触发"的流水线模式。仓库的一致性测试 conformance_task_group.cpp 给出了直接可用的验证用例:
oneapi::tbb::task_handle h; h = tg.defer([&]{ run = true; }); CHECK_MESSAGE(run == false, "delayed task should not be run until run(task_handle) is called"); tg.run(std::move(h));测试同时验证了:默认构造的task_handle为空(h == nullptr)、defer返回的句柄非空、task_handle不可拷贝(见 conformance_task_group.cpp),以及延迟任务会延长wait()的等待——即便没有任何工作线程,wait()也不会在延迟任务被调度前提前返回。
run_and_wait:提交并立即等待
template<typename Func> task_group_status run_and_wait(const Func& f); task_group_status run_and_wait(task_handle&& h);两个重载分别等价于{run(f); return wait();}与{run(h); return wait();}——即先提交任务(函数体或句柄指向的延迟任务),然后阻塞等待组内全部任务完成。它们返回任务组最终状态task_group_status。
从实现看,run_and_wait走的是基类 task_group.h 的internal_run_and_wait路径:通过try_call包装执行过程,on_completion回调中检测上下文是否收到取消请求,据此返回canceled或complete,最后调用context().reset()将上下文复位以便复用。
对句柄版本,规范同样要求h非空且属于*this任务组,否则未定义行为(实现中以断言拦截,见 task_group.h)。
wait:等待完成,同时不浪费线程
task_group_status wait();wait()等待组内所有任务完成或全部被取消,返回最终状态。它的关键特性(也是 oneTBB 与裸线程 join 的本质区别)是:调用wait()的线程在等待期间会转而执行其他可运行的任务——包括与本任务组无关的任务。这让阻塞等待变成一种"协作式"参与,显著提升线程利用率。
对应实现见基类 task_group.h:
task_group_status wait() { bool cancellation_status = false; try_call([&] { d1::wait(m_wait_vertex.get_context(), context()); }).on_completion([&] { cancellation_status = m_context.is_group_execution_cancelled(); context().reset(); }); return cancellation_status ? canceled : complete; }因为task_group默认构造时启用了concurrent_wait特质,所以多个线程可以同时对同一个任务组调用wait(),实现多线程共同等待一组任务的完成(例如多个生产者线程等待共享工作队列清空)。
cancel:取消整组任务
void cancel();cancel()向任务组发出取消请求,组内所有任务(包括尚未开始执行的)都会进入取消流程。cancel()是幂等且线程安全的:重复调用或与执行中的任务并发调用都是安全的;已经启动的任务会在下一个取消检查点被终止,正在等待wait()/run_and_wait()的调用方会收到canceled状态。
实现上,cancel()调用context().cancel_group_execution()(见 task_group.h),底层转发到r1::cancel_group_execution,通过原子标记my_cancellation_requested完成一次性的取消传播。
非成员函数:is_current_task_group_canceling
bool is_current_task_group_canceling();返回布尔值:若当前线程正在执行的最内层 task_group 正处于取消(cancelling)流程中,则为true,否则为false。该函数主要用于任务函数体内部的自检——例如在长时间运行的循环中定期查询,发现取消信号后主动提前退出,从而让取消更及时、资源回收更干净。
其实现非常简洁(见 task_group.h):
inline bool is_current_task_group_canceling() { task_group_context* ctx = current_context(); return ctx ? ctx->is_group_execution_cancelled() : false; }task_group_status:三态结果
wait()、run_and_wait()返回的task_group_status是定义于 task_group_status_enum.rst 的枚举类型:
enum task_group_status { not_complete, // 未被取消,且组内任务尚未全部完成 complete, // 未被取消,且组内任务已全部完成 canceled // 任务组已收到取消请求 };| 枚举值 | 含义 |
|---|---|
not_complete | 未收到取消请求,且并非所有任务都已完成(可用于中间状态判断) |
complete | 未收到取消请求,且所有任务均已完成 |
canceled | 任务组收到了取消请求 |
注意:run_and_wait/wait在正常路径下只会返回complete或canceled;not_complete更常见于需要区分"尚未结束"的扩展场景(如task_completion_handle的查询接口)。枚举的实际定义位于 _task_handle.h。
任务组上下文:task_group_context 与取消传播
task_group(task_group_context& context)允许将任务组绑定到显式上下文。task_group_context(见 task_group.h)是 oneTBB 调度器管理取消与异常传播的核心对象,其关键设计包括:
- 树形结构:上下文可以绑定到另一个上下文(parent → this → children),取消请求沿父向子方向传播;组内任一任务被取消,会级联取消所有绑定到它的子组任务;
- 两种 kind:
bound(默认):绑定到当前执行任务的上下文,随父组一起被取消,适合嵌套并行算法——内层算法发生异常时希望外层一并取消;isolated:与任何其他上下文隔离,创建开销更低,适合从外部线程直接调用的算法(不依赖外层取消语义时更高效);
- 两种 trait:
fp_settings:捕获/恢复浮点环境控制字;concurrent_wait:允许并发调用wait()(task_group默认构造即启用此特质);
- 异常拦截:任务执行中未捕获的异常会被上下文截获,并转化为内部的取消请求向组内传播,最终在
wait()调用点重新抛出。
源码级实现剖析:一个任务从提交到完成的全链路
结合 task_group.h 与 _task_handle.h,可以勾勒出任务的生命周期:
- 构造:
run(f)或defer(f)通过prepare_task/prepare_task_handle用small_object_allocator创建function_task(见 task_group.h),它内部持有可调用对象F与所属上下文引用; - 登记:任务构造时对组的等待树顶点
m_wait_vertex执行reserve(),这就是"组内任务数 +1"的计数来源; - 调度:
run调用d1::spawn把任务交给调度器;defer则先把function_task包进task_handle(unique_ptr 包装),直到run(task_handle&&)才release并 spawn; - 执行:工作线程执行
function_task::execute,在__TBB_ASSERT(ed.context == &this->ctx())处校验任务始终运行在自己的组上下文中,调用用户函数体,最后destroy(&ed)释放任务对象; - 收尾:任务完成时释放等待树顶点的引用;当所有任务(含延迟任务)都完成或取消后,
wait()的等待计数归零并唤醒; - 异常/取消:若任务抛出异常,上下文将其捕获并转为取消传播;
cancel()通过原子标记使组内任务在检查点取消。
基类析构中missing_wait异常的抛出(见 task_group.h)保证了"未 wait 即销毁"会被显式暴露,这一点配合一致性测试中的异常处理用例(见 conformance_task_group.cpp)构成了完整的生命周期契约。
实战示例:Sudoku 求解器
仓库提供了使用task_group接口的完整示例程序 task_group/sudoku/sudoku.cpp,其功能是计算数独棋盘的全部解(相关示例说明见 examples/task_group/README.md)。该示例非常适合作为阅读 task_group 实战用法的起点:它在搜索树的每个分支点动态地run出新任务来并行探索解空间,是"执行期间动态添加任务"这一核心能力的典型体现。
使用约束与最佳实践小结
综合规范文档与源码,使用task_group时需牢记以下要点:
- 必须 wait:销毁前调用
wait()(或run_and_wait),否则析构抛missing_wait异常; - 句柄归属:
task_handle只能交给创建它的任务组(run/run_and_wait),且不可为空,违反即未定义行为; - 句柄不可拷贝:
task_handle仅支持移动,复制会被编译器拒绝(测试已显式验证,见 conformance_task_group.cpp); - 延迟任务计入等待:
defer创建的任务即使未调度,也会让wait()持续等待,直到句柄被run或销毁; - 取消是协作式的:任务体内定期检查
is_current_task_group_canceling(),可让取消及时生效; - 嵌套并行用 bound:默认绑定上下文会让异常/取消沿树传播,内层算法异常时外层一并收尾,避免悬挂任务;
- wait 不浪费线程:等待线程会参与执行其他任务,因此用
wait()代替忙等/裸 join,是提高整体吞吐的正确姿势。
以上结论均可在一处对照验证:规范文档 task_group_cls.rst、声明头文件 oneapi/tbb/task_group.h 与一致性测试 conformance_task_group.cpp 三者在语义上完全一致,是学习与二次开发 oneTBB 任务调度的权威参考。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考