news 2026/10/21 0:39:41

oneTBB task_group 完全指南:任务组 API、动态依赖与预览扩展详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oneTBB task_group 完全指南:任务组 API、动态依赖与预览扩展详解
  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载

导读

task_group是 oneTBB(oneAPI Threading Building Blocks)中用于表达"一组任务并发执行"的核心调度原语:你可以在组执行期间动态地向其中添加任务,并通过wait()/run_and_wait()等待组内全部任务结束,通过cancel()取消整组任务。本文基于当前仓库的官方参考文档 task_group_cls.rst,完整讲解task_group的构造函数、run/defer/run_and_wait/wait/cancel等全部成员函数,并深入解析三个预览特性(Task Bypass、Dynamic Dependencies、Waiting for Individual Tasks)及配套类型task_handle、task_completion_handle、task_group_status。读完本文,你将能够用task_group编写动态任务图、实现带依赖的并行规约、按需等待单个任务完成,并理解其底层源码实现与测试验证方式。

一、task_group 是什么:核心概念与类声明

task_group代表一组任务的并发执行。与一次性的parallel_for等算法不同,它的两个关键特性是:

  • 动态添加:在组仍在执行期间,可以随时向组内添加新任务;
  • 线程可参与:调用task_group::wait()的线程,可能会转而执行与本task_group无关的其他任务(这是 oneTBB 工作窃取式调度器的特性,意味着等待线程不会空转)。

该类的完整声明位于头文件 oneapi/tbb/task_group.h(仓库实际实现中,公开 API 通过内联命名空间v1暴露于tbb命名空间,早期文档亦写作oneapi::tbb):

// 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(); // Preview feature: Waiting an Individual Task task_group_status wait_for_task(task_completion_handle& ch); task_group_status run_and_wait_for_task(task_handle&& h); task_group_status get_status_of(task_completion_handle& ch); // Preview feature: Dynamic Dependencies static void set_task_order(task_handle& pred, task_handle& succ); static void set_task_order(task_completion_handle& pred, task_handle& succ); static void transfer_this_task_completion_to(task_handle& h); }; bool is_current_task_group_canceling(); } // namespace tbb } // namespace oneapi

在源码中,task_group派生自内部基类task_group_base,基类持有两个关键成员(task_group.h):

  • d1::wait_context_vertex m_wait_vertex—— 等待树的顶点,用于协调wait()的完成计数;
  • d1::task_group_context m_context—— 组上下文,承载取消状态与异常传播。

二、构造与析构:空组、外部上下文与 missing_wait

默认构造task_group()

构造一个空的task_group,即组内没有任何任务。从源码看,默认构造还额外启用了concurrent_waittrait(task_group.h),允许多个线程并发调用wait()。

指定上下文构造task_group(task_group_context& context)

构造一个空组,但组内所有任务都与传入的context关联。这意味着任务的取消、异常处理、优先级等语义由该上下文决定。task_group_context支持bound(绑定到当前执行环境的父上下文,取消可向上/向下传播)与isolated(完全隔离)两种类型,参见 task_group.h 的构造函数及类注释。

析构~task_group():必须先行 wait

析构函数有一个硬性要求:

Requires: 销毁task_group之前必须调用wait(),否则析构函数会抛出异常。

源码中的实现(task_group.h)在析构时检测等待顶点是否仍在执行:若组内还有任务未完成,会先尝试cancel()清理并等待所有任务退出,然后抛出missing_wait异常(throw_exception(exception_id::missing_wait))。换言之,忘记调用wait()不是静默的资源泄漏,而是直接抛异常,从而避免更严重的内存破坏。

三、添加任务:run 与 defer

void run(Func&& f)—— 立即调度

向组内添加一个计算f()的任务并立即返回,任务的执行交给调度器异步进行。Func必须满足 ISO C++ 标准 [function.objects] 一节定义的函数对象要求(即可调用、可复制构造)。

源码实现非常直接(task_group.h):通过prepare_task把函数对象包装成内部function_task,然后d1::spawn(*prepare_task(std::forward<F>(f)), context())将其投递到调度器。

task_handle defer(F&& f)—— 延迟创建任务

创建计算f()的延迟任务并返回指向它的task_handle。关键语义:

  • 任务不会被自动调度执行,直到显式请求(例如通过task_group::run(task_handle&&));
  • 但任务已经加入该task_group,因此wait()会一直等待该task_handle被调度或销毁;
  • F同样必须满足函数对象要求;
  • 返回值是task_handle,它"拥有"这个延迟任务对象。
tbb::task_group tg; tbb::task_handle h = tg.defer([&]{ /* 延迟执行的工作 */ }); // 此刻任务并未运行 tg.run(std::move(h)); // 显式调度 tg.wait(); // 等待完成

void run(task_handle&& h)—— 调度延迟任务

调度h指向的任务对象执行。注意是移动语义:h在调用后变为空。若违反以下条件,行为是未定义(undefined behavior):

  • h为空;
  • *this不是创建h的那个task_group。

源码中(task_group.h)用__TBB_ASSERT同时检查这两条前置条件,并通过task_handle_accessor::release(h)取出内部任务后d1::spawn调度。

关于task_handle:它本质上是一个"延迟任务的所有权句柄",可移动、不可拷贝,支持与nullptr比较来判断是否为空(默认构造为空、移动后原句柄为空)。完整接口见 task_handle.rst 与头文件实现。

四、等待与取消:run_and_wait / wait / cancel

task_group_status run_and_wait(const Func& f)

等价于{run(f); return wait();},即先提交任务,然后立即等待该任务(以及组内所有其他任务)完成。返回组的最终状态。

task_group_status run_and_wait(task_handle&& h)

等价于{run(h); return wait();}。前置条件与run(task_handle&&)相同:h非空、*this是h的所属组。

源码中,run_and_wait最终落到task_group_base::internal_run_and_wait(task_group.h):调用execute_and_wait执行任务并等待,完成后检查context().is_group_execution_cancelled()决定返回canceled还是complete,同时调用context().reset()复位上下文以便复用。

task_group_status wait()

等待组内所有任务完成或全部被取消。实现见 task_group.h:d1::wait(m_wait_vertex.get_context(), context())阻塞至组内任务计数归零,随后同样以"是否收到取消请求"决定返回值。

void cancel()

取消本task_group内的所有任务。实现是context().cancel_group_execution()(task_group.h)。取消是协作式的:任务体内部的取消检查点(如is_current_task_group_canceling())会感知到取消并尽快退出;被取消的组wait()返回canceled。

配套类型task_group_status

wait()/run_and_wait()返回的task_group_status是枚举类型,完整定义见 task_group_status_enum.rst:

枚举值含义
not_complete未取消且组内尚有任务未完成
complete未取消且组内所有任务已完成
canceled任务组收到了取消请求
task_complete(预览特性)单个任务已执行完成

测试 conformance_task_group.cpp 验证了取消语义:外层组cancel()后wait()返回canceled,而使用isolated上下文的内层组依然返回complete且全部任务执行完毕——这正是"隔离上下文不受外层取消影响"的行为佐证。此外 L174-L191 验证了任务体内抛出的异常会通过wait()向调用方传播。

五、非成员函数:is_current_task_group_canceling

bool is_current_task_group_canceling();

若当前线程正在执行的、最内层的task_group正在取消其任务,则返回true,否则返回false。它通常用在任务体内部,作为协作式取消的检查点:

tg.run([&]{ while (!tbb::is_current_task_group_canceling()) { // 做一小段工作后再次检查取消状态 } });

源码实现(task_group.h)取当前线程的上下文current_context(),再调用is_group_execution_cancelled()判断取消标志。

六、预览特性总览与启用方式

以下是官方文档 task_group_cls.rst 列出的三个扩展task_groupAPI 的预览特性,均为预览(preview)功能,需要在使用前定义宏TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1(一般放在#include之前):

预览特性能力参考文档
Task Bypass Support允许任务体返回task_handle,作为"下一步应执行哪个任务"的提示task_bypass.rst
Dynamic Dependencies允许在task_group内为任务设置执行依赖(前驱-后继关系)dynamic_dependencies.rst
Waiting for Individual Tasks允许等待组内单个任务完成wait_single_task.rst

启用后,特性测试宏TBB_HAS_TASK_GROUP_DEPENDENCIES、TBB_HAS_TASK_GROUP_WAIT_FOR_SINGLE_TASK会被定义,可用于编译期判断。

七、预览特性一:Task Bypass(任务旁路)

语义

该特性放宽了对任务函数对象返回类型的要求:F可以返回一个task_handle对象。若返回的句柄非空、且其拥有的任务没有依赖,则该句柄作为"下一个可执行任务"的优化提示,帮助调度器减少任务调度开销(工作窃取场景下的任务旁路)。

需注意的约束:

  • 返回句柄所代表任务的执行不保证立即发生,也不保证由同一线程执行;
  • 返回的task_handle不得再显式提交(如再次传给run),否则行为未定义;
  • 若返回句柄由其他task_group创建,行为未定义。

源码级原理

在 task_group.h 的function_task::execute中,任务体执行后调用task_ptr_or_nullptr(m_func):若任务体返回了task_handle,则提取其内部任务指针;在同时存在旁路任务与后继任务时,后继任务被spawn,旁路任务作为execute的返回值交给调度器直接执行(实现于d2::task_ptr_or_nullptr_impl,见 L131-L157,其中release_dependency保证只有无未决依赖的任务才能被旁路)。

示例:并行 for_each(分治)

完整可运行示例见 task_group_extensions_bypassing.cpp,核心逻辑如下:

#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1 #include "oneapi/tbb/task_group.h" template <typename Iterator, typename Function> struct for_task { tbb::task_handle operator()() const { tbb::task_handle next_task; auto size = end - begin; if (size < serial_threshold) { for (Iterator it = begin; it != end; ++it) f(*it); // 串行执行 } else { Iterator middle = begin + size / 2; tg.run(for_task<Iterator, Function>{middle, end, f, tg}); // 提交右半段 next_task = tg.defer(for_task<Iterator, Function>{begin, middle, f, tg}); // 旁路左半段 } return next_task; } Iterator begin; Iterator end; Function f; tbb::task_group& tg; }; template <typename RandomAccessIterator, typename Function> void par_for_each(RandomAccessIterator begin, RandomAccessIterator end, Function f) { tbb::task_group tg; tg.run_and_wait(for_task<RandomAccessIterator, Function>{begin, end, std::move(f), tg}); }

每个分治任务在"串行阈值(示例中serial_threshold = 16)内直接干活,否则拆半:右半段用run提交给调度器,左半段用defer创建并作为返回值旁路,从而在当前线程上顺势继续执行左半段,降低任务切换开销。

八、预览特性二:Dynamic Dependencies(动态依赖)

基本概念

该特性为task_group增加了前驱-后继(predecessor-successor)任务依赖能力:后继任务只有在其所有前驱都完成后才能开始执行。三类关键术语:

  • 未提交任务(unsubmitted):尚未提交执行的任务;非空task_handle即代表一个未提交任务;
  • 已提交任务(submitted):已提交执行的任务(例如task_handle已传给run);
  • 前驱可以是已提交或未提交任务,后继只能是未提交任务。

API 与约束

static void set_task_order(pred, succ):注册依赖,使succ必须等pred完成后才能开始执行。以下情况行为未定义:

  • pred或succ为空;
  • 二者属于不同的task_group实例;
  • pred是task_completion_handle且对应任务未提交就被销毁。

它支持并发操作:可并发地为同一后继添加多个前驱、为同一前驱注册多个后继;也可在任务已提交甚至完成后继续通过task_completion_handle添加后继。

static void transfer_this_task_completion_to(task_handle& h):将当前正在执行的任务的完成事件转移给h指向的任务。转移后,当前任务的所有后继会被重新挂到接收任务上,即后继们要等接收任务完成后才执行。约束:必须在任务体内调用、h非空、不得对已完成转移的任务再次转移、两任务必须属于同一task_group。

源码实现

set_task_order的实现(task_group.h)取出前驱任务对应的task_dynamic_state,调用pred_state->add_successor(succ)把后继挂入等待树;transfer_this_task_completion_to(L699-L707)则从current_task_ptr()取当前任务并调用transfer_completion_to(new_task)。依赖任务在run(task_handle&&)时(L650-L655)会检查has_dependencies()与release_dependency():若有未决依赖则暂不 spawn,等待前驱完成后再调度。

示例:基于依赖的并行规约

完整示例见 task_group_extensions_reduction.cpp,展示了"分治 + 依赖 + 完成转移"的组合用法:

#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, right; }; tbb::task_handle operator()() const { tbb::task_handle next_task; std::size_t size = end - begin; if (size < serial_threshold) { for (std::size_t i = begin; i < end; ++i) result += i; // 串行规约 } else { 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); // join 等左叶子 tbb::task_group::set_task_order(right_leaf, join); // join 等右叶子 tbb::task_group::transfer_this_task_completion_to(join); // 当前任务完成转给 join next_task = std::move(left_leaf); // 左叶子用于旁路 tg.run(std::move(right_leaf)); tg.run(std::move(join)); } return next_task; } std::size_t begin, 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; }

这里join任务通过set_task_order同时依赖左右两个叶子任务;当前任务调用transfer_this_task_completion_to(join)把自己的"完成"转给join,使所有等待当前任务完成的后继都改为等待join;左叶子被返回用于旁路,右叶子与join显式run。最终形成一棵可在多线程间动态拆分的规约树。

九、预览特性三:Waiting for Individual Tasks(等待单个任务)

与 task_completion_handle 的配合

等待单个任务需要先引入配套类型task_completion_handle(详见 task_completion_handle.rst)。它与task_handle的关键区别:

  • task_handle在任务提交后变空;
  • task_completion_handle则无论任务处于未提交、执行中还是已完成状态,都持续引用该任务,可复制(多个句柄可同时引用同一任务),也支持与nullptr比较。

典型生命周期:

tbb::task_group tg; tbb::task_handle th = tg.defer(task_body); // 未提交,th 非空 tbb::task_completion_handle tch = th; // 未提交,th 与 tch 均非空 tg.run(std::move(th)); // 已提交,th 变空,tch 仍引用该任务 tg.wait(); // 已结束,tch 仍引用该(已完成的)任务

新增成员函数

task_group_status wait_for_task(task_completion_handle& ch):阻塞直到ch代表的任务完成。返回task_complete(任务被执行)或canceled(未执行)。其他组内任务可能仍在运行——这是与wait()的本质区别。

task_group_status run_and_wait_for_task(task_handle&& h):提交任务(若其无未决依赖)并等待其完成,语义上等价于task_completion_handle ch = h; run(std::move(h)); wait_for_task(ch);。

task_group_status get_status_of(task_completion_handle& ch):非阻塞地查询单个任务状态:not_complete(未提交或未完成)、task_complete(已执行完成)、canceled(因组取消而未执行)。

若任务完成事件已通过transfer_this_task_completion_to转移,上述所有等待函数都会改为跟踪接收完成事件的那个任务。

task_arena也获得对应扩展:task_arena::wait_for(task_completion_handle&)等价于在 arena 内对所属task_group执行wait_for_task(见 wait_single_task.rst)。源码实现位于 task_group.h(wait_for_task)与 L542-L556(internal_run_and_wait_for_task)。

示例:带缓存的异步计算

完整示例见 task_group_extensions_wait_for_one.cpp,核心逻辑:

#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; if (cache_lookup(input, result)) return result; // 缓存命中直接返回 tbb::task_handle calculate_task = tg.defer(calculate_result(input, result)); tg.run_and_wait_for_task(std::move(calculate_task)); // 计算并等它完成 tg.run(cache_result(input, result)); // 缓存写入异步进行,不等待 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]); } tg.wait(); // 确保所有异步缓存写入完成 clear_cache(); }

这个模式的价值在于:run_and_wait_for_task让调用方拿到计算结果的同时,缓存写入任务仍在组内异步运行;最后统一tg.wait()收尾,保证清空缓存前所有后台任务已结束。

十、测试验证:conformance 测试要点

官方一致性测试 conformance_task_group.cpp(对应规范标签[scheduler.task_group])从行为层面对上述语义做了严格验证,可作为理解 API 契约的补充:

  • 延迟任务不会提前执行(L33-L46):defer创建的任务在run之前run标志必须仍为false;
  • run + wait 语义(L50-L67):run(std::move(h))后wait()返回时任务必已执行,且h变空(h == nullptr);
  • run_and_wait 语义(L71-L87):提交并等待后任务完成、句柄为空;
  • 空句柄判断(L89-L120):默认构造的task_handle为空,defer返回的非空,且支持双向==/!=与nullptr比较;
  • 不可拷贝(L125-L130):task_handle既不可拷贝构造也不可拷贝赋值(static_assert编译期验证);
  • defer 会延长 wait(L133-L172):即使设置了max_allowed_parallelism = 1,wait()也会一直阻塞到run(h)被调用,验证"延迟任务仍计入组等待集合";
  • 异常传播(L174-L191):任务体抛出异常时wait()会重新抛出;
  • 取消与隔离上下文(L206-L274):外层cancel()后wait()返回canceled,而isolated上下文的内层组仍完整执行并返回complete。

十一、小结与最佳实践

  1. 先 wait 再析构:task_group析构要求先调用wait(),否则抛出missing_wait异常——始终在作用域结束前显式wait();
  2. 动态任务图:run可随时添加任务,defer+run(task_handle&&)可精确控制调度时机;
  3. 取消是协作式的:在长任务体中使用is_current_task_group_canceling()做检查点,配合cancel()实现快速退出;
  4. 预览特性需显式开启:#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1,并注意其 API 仍在演进(task_complete枚举值、set_task_order等均为预览形态),生产使用前应关注版本更新与 RELEASE_NOTES.md;
  5. 依赖要合法:set_task_order要求前驱后继同属一个task_group、句柄非空;违反即未定义行为;
  6. 单任务等待:当只需要等待某个任务的结果而不想阻塞整个组时,优先使用run_and_wait_for_task/wait_for_task+task_completion_handle。

更多配套参考:task_group_status枚举见 task_group_status_enum.rst,task_handle见 task_handle.rst,task_completion_handle见 task_completion_handle.rst;完整头文件实现位于 oneapi/tbb/task_group.h。

  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载

相关推荐

上一篇:FlutterFire查询优化清单:提高Firebase查询性能的步骤
下一篇:5分钟掌握tchMaterial-parser:中小学教育资源获取终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 4:20:10

蘑菇采摘机器人视觉方案:从检测到抓取点计算与手眼标定

简介&#xff1a;这份PDF文献《计算机视觉在蘑菇采摘机器人上的应用》面向农业工程、机器人及计算机视觉方向的学习者与研究人员&#xff0c;聚焦蘑菇工业化生产中采摘与分类环节的自动化难题。文中系统介绍了采摘机器人的整体工作流程&#xff0c;并重点剖析其计算机视觉系统的…

作者头像 李华
网站建设 2026/10/15 10:58:27

随机化学算法驱动的电力系统级联故障风险评估与Matlab实现

做电力系统风险分析的人&#xff0c;多半都遇到过这种尴尬&#xff1a;穷举N-1校核不过瘾&#xff0c;想往上摸N-2甚至N-3&#xff0c;组合数直接爆炸&#xff1b;用蒙特卡洛慢慢抽&#xff0c;抽了几万次&#xff0c;高风险的“倒霉场景”还是稀稀拉拉。这篇文章要把我最近在做…

作者头像 李华
网站建设 2026/10/14 10:54:36

HTML注释的底层原理、常见坑与工程化实践详解

直接聊点实际的——HTML注释&#xff0c;这个话题我做了十年前端也没敢说完全搞明白了。很多新手觉得注释不就是<!-- -->包一下吗&#xff1f;但我在真实项目里见过太多因为注释翻车的案例&#xff1a;有人一个注释嵌套把整页样式搞崩了&#xff0c;有人把内网接口地址写…

作者头像 李华
网站建设 2026/10/14 1:37:44

蓝桥杯交易账本题解:差分数组建模撤销与查询

2023 年蓝桥杯国赛的“交易账本”&#xff0c;赛后好几个人问我思路。这道题题面读起来绕&#xff0c;尤其那个“撤销操作”&#xff0c;一旦理解歪了&#xff0c;实现起来就是灾难。我自己第一版代码就吃过亏&#xff0c;当时把所有操作当成“时间轴上的一次性变化”来处理&am…

作者头像 李华