- 并发编程
- 高性能计算
【免费下载链接】oneTBB
oneAPI Threading Building Blocks (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。
十一、小结与最佳实践
- 先 wait 再析构:
task_group析构要求先调用wait(),否则抛出missing_wait异常——始终在作用域结束前显式wait(); - 动态任务图:
run可随时添加任务,defer+run(task_handle&&)可精确控制调度时机; - 取消是协作式的:在长任务体中使用
is_current_task_group_canceling()做检查点,配合cancel()实现快速退出; - 预览特性需显式开启:
#define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1,并注意其 API 仍在演进(task_complete枚举值、set_task_order等均为预览形态),生产使用前应关注版本更新与 RELEASE_NOTES.md; - 依赖要合法:
set_task_order要求前驱后继同属一个task_group、句柄非空;违反即未定义行为; - 单任务等待:当只需要等待某个任务的结果而不想阻塞整个组时,优先使用
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)
相关推荐
电视盒子刷Armbian的方法:30分钟完整把S905改成Linux服务器
电视盒子刷Armbian的方法:30分钟完整把S905改成Linux服务器 跟着本教程走,你可以把一台 E900V21D(晶晨 S905L3 B)安卓电视盒子刷
并发编程高性能计算如何无水印批量下载抖音作品?douyin-downloader 实操指南
如何无水印批量下载抖音作品?douyin downloader 实操指南 想把手上那位博主的主页视频整段留底,又怕一条条手动保存太费劲、还要忍受满屏水印?dou
网页爬虫CLILoop:免费开源的 macOS 窗口管理工具,3 分钟装好并完成第一次半屏摆放
Loop:免费开源的 macOS 窗口管理工具,3 分钟装好并完成第一次半屏摆放 Loop 是一款免费开源的 macOS 窗口管理工具,把 macOS 窗口管理
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考