mold 仓库中的 oneAPI TBB task_completion_handle 指南:动态任务依赖与单任务完成等待的完整实现解析
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
导读
task_completion_handle是 oneAPI TBB(Threading Building Blocks)任务组扩展中的核心句柄类型,用于表示一个任务以便设置执行依赖并追踪其完成状态。与task_handle提交后即变空不同,task_completion_handle无论任务处于已提交、执行中还是已完成状态,都始终持有对该任务的引用。本文以 task_completion_handle_cls.rst 为骨架,结合仓库内 TBB 头文件源码(_task_handle.h、task_group.h、task_arena.h)与官方示例,完整讲解其 API、与动态依赖及单任务等待两大扩展功能的协同关系,使读者能够掌握在 TBB 任务组中建立动态依赖图、等待单个任务完成以及转移任务完成点的完整实战能力。
说明:本仓库(mold 链接器项目)将 TBB 作为第三方依赖引入(位于
third-party/tbb),本文讨论的task_completion_handle即来源于该依赖,是理解任务组扩展功能的参考文档。
预备知识:扩展宏与头文件
该功能属于preview 扩展,使用前必须定义宏:
#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include <oneapi/tbb/task_group.h>在task_completion_handle之外,同一扩展还提供了两个配套特性:
- 动态依赖(Dynamic Dependencies):通过
task_group::set_task_order建立任务间前驱/后继关系,见 dynamic_dependencies.rst; - 单任务等待(Waiting for Individual Tasks):通过
wait_for_task/run_and_wait_for_task/get_status_of等待单个任务完成,见 wait_single_task.rst。
当宏启用时,编译器会额外定义特性测试宏:
TBB_HAS_TASK_GROUP_DEPENDENCIES:动态依赖可用;TBB_HAS_TASK_GROUP_WAIT_FOR_SINGLE_TASK:单任务等待可用。
从源码看,该宏控制着 task_group.h 中run(task_handle&&)的依赖检查分支,以及task_group_status枚举中的task_complete成员(见 _task_handle.h):
enum task_group_status { not_complete, complete, canceled #if __TBB_PREVIEW_TASK_GROUP_EXTENSIONS , task_complete #endif };task_completion_handle 与 task_handle 的本质区别
理解task_completion_handle的关键,是它与task_handle生命周期语义的不同。官方文档给出了一个逐步演示两者差异的代码序列:
tbb::task_group tg; tbb::task_handle th = tg.defer(task_body); // task is not submitted // th is non-empty and represents the task tbb::task_completion_handle tch = th; // task is not submitted // both th and tch are non-empty and represent the task tg.run(std::move(th)); // task is submitted // th is empty // tch is non-empty and keeps representing the task tg.wait(); // task is completed // tch is non-empty and represents the completed task可以看到:
task_handle是"一次性"所有权句柄,一旦通过run提交任务,句柄即变为空;task_completion_handle是"跟踪型"句柄,任务从创建到提交再到完成,它始终非空并保持对该任务的引用。
从源码层面看,这种差异源于两者的内部表示(_task_handle.h):
task_handle内部持有std::unique_ptr<task_handle_task, task_handle_task_deleter>,提交时通过task_handle_accessor::release释放底层任务指针(见 task_group.h),因此提交后句柄变空;task_completion_handle内部持有指向task_dynamic_state的指针m_task_state,通过引用计数(reserve()/release())与任务动态状态建立"共同所有权"关系(见 _task_handle.h),从而在任务提交、执行、完成后都能继续引用它。
task_completion_handle的两大用途:
- 建立动态任务依赖:可作为
task_group::set_task_order的前驱,随时添加后继任务,即使对应任务已经提交甚至已完成; - 等待单个任务完成:通过
task_group::wait_for_task等函数单独等待该任务,而不必等待组内所有任务结束。
非空task_completion_handle的获取途径:从非空task_handle构造或赋值、复制另一个非空的task_completion_handle。多个task_completion_handle可以同时引用同一个任务;空的task_completion_handle不引用任何任务。
完整 API 参考
头文件与类声明
#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include <oneapi/tbb/task_group.h> namespace oneapi { namespace tbb { class task_completion_handle { public: task_completion_handle(); task_completion_handle(const task_handle& handle); task_completion_handle(const task_completion_handle& other); task_completion_handle(task_completion_handle&& other); ~task_completion_handle(); task_completion_handle& operator=(const task_handle& handle); task_completion_handle& operator=(const task_completion_handle& other); task_completion_handle& operator=(task_completion_handle&& other); explicit operator bool() const noexcept; friend bool operator==(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept; friend bool operator!=(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept; friend bool operator==(const task_completion_handle& t, std::nullptr_t) noexcept; friend bool operator!=(const task_completion_handle& t, std::nullptr_t) noexcept; friend bool operator==(std::nullptr_t, const task_completion_handle& t) noexcept; friend bool operator!=(std::nullptr_t, const task_completion_handle& t) noexcept; }; // class task_completion_handle } // namespace tbb } // namespace oneapi需要说明的是:源码中的类位于tbb::detail::d2命名空间(见 _task_handle.h),而公共头文件通过using detail::d2::task_completion_handle;将其暴露在oneapi::tbb命名空间(见 task_group.h)。
构造函数
task_completion_handle();默认构造一个空的task_completion_handle,不引用任何任务。
task_completion_handle(const task_handle& handle);构造引用handle所关联任务的task_completion_handle。若handle为空,行为未定义(UB)。源码中通过断言__TBB_ASSERT(th, ...)检查这一前提,并调用get_dynamic_state()惰性创建任务的动态状态,同时reserve()增加一份共同所有权引用(_task_handle.h)。
task_completion_handle(const task_completion_handle& other);拷贝构造。拷贝后*this与other引用同一个任务,other的m_task_state被复制并reserve()(新增一份共同所有权引用),因此两个句柄可同时存活。
task_completion_handle(task_completion_handle&& other);移动构造。移动后*this引用other原先引用的任务,other被置空(other.m_task_state = nullptr),不产生新的所有权引用。
析构函数
~task_completion_handle();销毁句柄。若其仍引用某任务,则调用m_task_state->release()释放共同所有权引用;当引用计数归零时,动态状态对象被销毁(_task_handle.h)。
赋值操作符
task_completion_handle& operator=(const task_handle& handle);将*this引用的任务替换为handle关联的任务。若handle为空,行为未定义。返回*this。源码实现在替换前先释放旧状态的引用,再对新状态reserve()(_task_handle.h)。
task_completion_handle& operator=(const task_completion_handle& other);拷贝赋值。赋值后两者引用同一任务。若新旧状态相同(m_task_state != other.m_task_state判断),则不做多余操作;否则先释放旧引用再获取新引用。返回*this。
task_completion_handle& operator=(task_completion_handle&& other);移动赋值。移动后*this引用other原先的任务,other置空。返回*this。
观察者
explicit operator bool() const noexcept;若*this引用某个任务则返回true,否则返回false。源码实现为m_task_state != nullptr。由于是explicit,只能用于条件判断等布尔上下文,不能隐式转换为整数。
比较操作
bool operator==(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept;若lhs与rhs引用同一个任务则返回true。源码通过比较m_task_state指针实现(_task_handle.h)。
bool operator!=(const task_completion_handle& lhs, const task_completion_handle& rhs) noexcept;等价于!(lhs == rhs)。
bool operator==(const task_completion_handle& t, std::nullptr_t) noexcept; bool operator==(std::nullptr_t, const task_completion_handle& t) noexcept;若t不引用任何任务则返回true(即空句柄判断)。
bool operator!=(const task_completion_handle& t, std::nullptr_t) noexcept; bool operator!=(std::nullptr_t, const task_completion_handle& t) noexcept;等价于!(t == nullptr)。
需要注意:在 C++20 及以后(__TBB_CPP20_COMPARISONS_PRESENT),部分比较运算符由编译器自动推导,源码中以条件编译控制(_task_handle.h)。
场景一:作为动态依赖的前驱
task_completion_handle最核心的价值体现在动态依赖功能中。相关 API 定义于 dynamic_dependencies.rst,要点如下:
- 未提交任务(unsubmitted):尚未提交执行的任务;
- 已提交任务(submitted):已通过
task_group::run等提交执行的任务; - 非空
task_handle只能表示未提交任务,而task_completion_handle可以表示任意状态的任务; - 已提交和未提交的任务都可以作为前驱,但只有未提交的任务才能作为后继。
set_task_order的两个重载:
static void set_task_order(task_handle& pred, task_handle& succ); static void set_task_order(task_completion_handle& pred, task_handle& succ);两者都调用pred_state->add_successor(succ)(task_group.h)。add_successor的源码实现(_task_handle.h)体现了该扩展的核心机制:
- 为后继任务动态状态
register_dependency()注册一个依赖计数; - 通过
notify_successor_node把后继挂到前驱的通知链表(notify list)上; - 前驱完成时,
fetch_list_and_notify_all(COMPLETED_FLAG)遍历链表,递减后继的依赖计数,最后一个依赖释放时,后继任务被spawn执行(_task_handle.h)。
因为依赖信息保存在任务自身的task_dynamic_state中而非task_handle中,所以即使前驱已被提交执行,只要持有其task_completion_handle就能随时添加后继——这正是"动态依赖"名称的由来。线程安全性方面:
- 可以并发地为同一后继添加多个前驱,也可以把同一前驱注册给多个后继;
- 可以在同一任务的
task_handle被run的同时,并发地向其task_completion_handle添加后继。
未定义行为的情形包括:
pred或succ为空;- 前后驱任务属于不同的
task_group实例; - 被
task_completion_handle引用的任务在未提交执行前就被销毁。
场景二:转移任务完成点
transfer_this_task_completion_to允许把当前正在执行任务的完成状态转移给另一个任务:
static void transfer_this_task_completion_to(task_handle& handle);该函数必须从任务体内调用。转移后,当前任务的所有后继会被重新挂接到接收完成状态的任务上,即后继任务要等接收者完成后才会执行。文档中的示例:
tbb::task_handle t = tg.defer([&tg] { tbb::task_handle comp_receiver = tg.defer(receiver_body); tbb::task_group::transfer_this_task_completion_to(comp_receiver); tg.run(std::move(comp_receiver)); }); tbb::task_handle succ = tg.defer(succ_body); tbb::task_group::set_task_order(t, succ); // Since t transfers its completion to comp_receiver, // succ_body will execute after receiver_body源码实现路径为transfer_this_task_completion_to→dynamic_state_task::transfer_completion_to→task_dynamic_state::transfer_completion_to(_task_handle.h):先把通知链表头 CAS 为TRANSFERRED_FLAG标记,取出整个通知链表,再整体并入接收任务的通知链表,并把新完成点记录在m_new_completion_point中。此后,无论是新增后继(add_successor)、等待完成(wait_for_completion)还是查询状态(get_task_status),一旦发现TRANSFERRED_FLAG都会自动重定向到接收任务。
未定义行为的情形:
handle为空;- 在
task_group任务体外调用; - 对已完成转移的任务再次调用;
- 当前任务与
handle关联任务属于不同task_group。
场景三:等待单个任务完成
配合task_completion_handle,TBB 还提供了单任务等待功能,定义于 wait_single_task.rst。它补充了传统task_group::wait()(等待组内所有任务)的粒度,让调用者只等待感兴趣的那一个任务。
相关 API
// <oneapi/tbb/task_group.h> synopsis namespace oneapi { namespace tbb { enum task_group_status { not_complete, complete, canceled, task_complete }; class task_group { public: task_group_status wait_for_task(task_completion_handle& comp_handle); task_group_status run_and_wait_for_task(task_handle&& handle); task_group_status get_status_of(task_completion_handle& comp_handle); }; } }// <oneapi/tbb/task_arena.h> synopsis namespace oneapi { namespace tbb { class task_arena { public: task_group_status wait_for(task_completion_handle& comp_handle); }; } }task_group_status 枚举语义
| 枚举值 | 语义 |
|---|---|
not_complete | 工作尚未完成。由get_status_of返回时表示该任务尚未完成;由wait/run_and_wait返回时表示组内并非所有任务都已完成 |
complete | 组未被取消且组内所有任务均已完成 |
canceled | 已收到取消请求。由get_status_of或单任务等待函数返回时表示该任务未被执行;由wait/run_and_wait返回时表示组执行被取消、各任务完成状态未知 |
task_complete | 该任务已完成执行。此时task_group的取消状态未知——即使组收到取消请求,个别任务仍可能被执行 |
成员函数详解
task_group_status wait_for_task(task_completion_handle& comp_handle);阻塞等待comp_handle代表的任务完成。若完成点已通过transfer_this_task_completion_to转移,则等待接收完成的任务。返回:任务被执行则返回task_group_status::task_complete,否则返回canceled。源码实现为state->wait_for_completion(context())(task_group.h),其内部通过notify_waiter_node把等待者挂入通知链表,然后d1::wait阻塞(_task_handle.h)。
task_group_status run_and_wait_for_task(task_handle&& handle);提交handle代表的任务(若无未解决依赖)并等待其完成。语义等价于:
task_completion_handle ch = handle; run(std::move(handle)); wait_for_task(ch);返回:task_complete或canceled。源码internal_run_and_wait_for_task(task_group.h)中,若任务仍有依赖且释放依赖后计数未归零,则只等待;否则直接执行自身并等待。
task_group_status get_status_of(task_completion_handle& comp_handle);不阻塞地查询任务状态:
not_complete:任务未提交或尚未执行完毕;task_complete:任务已执行完毕;canceled:任务因task_group取消而未执行。
若完成点被转移,则返回接收任务的状态。源码实现task_dynamic_state::get_task_status(_task_handle.h)直接检查通知链表头的哨兵标记(COMPLETED_FLAG/CANCELED_FLAG/TRANSFERRED_FLAG),无需阻塞。
task_group_status wait_for(task_completion_handle& comp_handle); // task_arena 成员在当前 arena 中等待comp_handle代表的任务完成。语义等价于在对应task_group上调用wait_for_task:execute([&] { tg.wait_for_task(comp_handle); })。返回:task_complete或canceled。该函数同样在完成点被转移时等待接收任务。
完整示例:缓存计算
官方示例 task_group_extensions_wait_for_one.cpp 演示了缓存场景:calculate_one_result先查缓存;缓存未命中时用run_and_wait_for_task立即计算并返回结果,同时把缓存写入作为异步任务提交到同一task_group;全部结果算完后tg.wait()确保后台缓存写入全部完成后再清空缓存:
#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include <oneapi/tbb/task_group.h> int calculate_one_result(tbb::task_group& tg, int input) { int result = 0; // Check cache first if (cache_lookup(input, result)) { return result; } // No cached item - run result calculation // May build task graph with dependencies tbb::task_handle calculate_task = tg.defer(calculate_result(input, result)); tg.run_and_wait_for_task(std::move(calculate_task)); // Result is calculated - run asynchronous caching tg.run(cache_result(input, result)); // Return the result to caller return result; } void calculate_all_results(const std::vector<int>& inputs, std::vector<int>& results) { tbb::task_group tg; for (std::size_t i = 0; i < inputs.size(); ++i) { results[i] = calculate_one_result(tg, inputs[i]); } // Wait for all incomplete caching tasks before clear tg.wait(); clear_cache(); }这个例子同时体现了"等待单个任务"(run_and_wait_for_task)与"等待组内全部任务"(tg.wait())两种粒度的配合使用。
综合实战:并行归约(Parallel Reduction)
动态依赖、完成点转移与task_completion_handle三者可以组合出完整的任务依赖图。官方示例 task_group_extensions_reduction.cpp 展示了用该 API 实现并行求和:
#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include "oneapi/tbb/task_group.h" struct reduce_task { struct join_task { void operator()() const { result = *left + *right; } std::size_t& result; std::unique_ptr<std::size_t> left; std::unique_ptr<std::size_t> right; }; tbb::task_handle operator()() const { tbb::task_handle next_task; std::size_t size = end - begin; if (size < serial_threshold) { // Perform serial reduction for (std::size_t i = begin; i < end; ++i) { result += i; } } else { // The range is too large to process directly // Divide it into smaller segments for parallel execution std::size_t middle = begin + size / 2; auto left_result = std::make_unique<std::size_t>(0); auto right_result = std::make_unique<std::size_t>(0); tbb::task_handle left_leaf = tg.defer(reduce_task{begin, middle, *left_result, tg}); tbb::task_handle right_leaf = tg.defer(reduce_task{middle, end, *right_result, tg}); tbb::task_handle join = tg.defer(join_task{result, std::move(left_result), std::move(right_result)}); tbb::task_group::set_task_order(left_leaf, join); tbb::task_group::set_task_order(right_leaf, join); tbb::task_group::transfer_this_task_completion_to(join); // Save the left leaf for further bypassing next_task = std::move(left_leaf); tg.run(std::move(right_leaf)); tg.run(std::move(join)); } return next_task; } std::size_t begin; std::size_t end; std::size_t& result; tbb::task_group& tg; }; std::size_t calculate_parallel_sum(std::size_t begin, std::size_t end) { tbb::task_group tg; std::size_t reduce_result = 0; tg.run_and_wait(reduce_task{begin, end, reduce_result, tg}); return reduce_result; }该示例的关键点:
set_task_order(left_leaf, join)与set_task_order(right_leaf, join)为join建立两个前驱依赖;transfer_this_task_completion_to(join)把当前(父)任务的完成点转移给join,使父任务的后继(若有)要等join完成;- 任务体内返回
task_handle使调度器可以进行任务旁路(bypass)优化,直接执行后继而无需重新入队。
与 task_arena 的集成
动态依赖扩展还涉及task_arena的两个enqueue重载(见 dynamic_dependencies.rst):
// <oneapi/tbb/task_arena.h> synopsis namespace oneapi { namespace tbb { class task_arena { // Only the behavior in case of dependent tasks is changed void enqueue(task_handle&& handle); }; namespace this_task_arena { // Only the behavior in case of dependent tasks is changed void enqueue(task_handle&& handle); } } }二者行为与泛型enqueue(F&& f)一致,仅参数类型不同。扩展语义为:若任务有前驱,则调度推迟到所有前驱完成后进行,函数立即返回。使用时handle必须非空,否则行为未定义。对应地,task_arena::wait_for(task_completion_handle&)用于在 arena 内等待单个任务完成(task_arena.h)。这使依赖图与 arena 的排队执行模型可以协同工作。
使用建议与注意事项
综合文档与源码,实践中需要特别注意:
- 必须先定义
TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1,且要在包含头文件之前定义; task_completion_handle从空task_handle构造或赋值是未定义行为,使用前应通过operator bool或与nullptr比较确认句柄非空;- 任务属于创建它的
task_group,set_task_order的跨组使用是未定义行为; - 被
task_completion_handle引用的任务不能未提交就被销毁,否则破坏动态状态的所有权关系; transfer_this_task_completion_to只能在任务体内调用,且不能重复转移;- 源码中
task_dynamic_state的引用计数机制(m_num_references)保证了task_handle与多个task_completion_handle可安全共存,析构顺序由计数自动管理(_task_handle.h); - 如需深入理解通知链表的无锁实现,可阅读
add_notify_node(CAS 插入 + 完成/取消/转移三种哨兵标记处理)与fetch_list_and_notify_all(遍历通知并批量唤醒后继)的源码(_task_handle.h)。
延伸阅读
- 类参考文档:task_completion_handle_cls.rst
- 动态依赖特性:dynamic_dependencies.rst
- 单任务等待特性:wait_single_task.rst
- 旁路(bypass)支持:task_bypass_support.rst
- 核心实现头文件:_task_handle.h
- 公共接口:task_group.h、task_arena.h
- 可运行示例:task_group_extensions_reduction.cpp、task_group_extensions_wait_for_one.cpp、task_group_extensions_bypassing.cpp
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考