mold 项目中的 oneTBB Flow Graph:异常处理与取消实战指南
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
导读
本指南围绕 mold 仓库所捆绑的 oneTBB(Threading Building Blocks)第三方库中的 Flow Graph 组件,系统讲解图执行过程中的异常传播、显式取消、图重置以及嵌套并行取消四大机制。结合 Flow-Graph-exception-tips.rst 文档与 _flow_graph_impl.h 源码,你将掌握:如何在节点体内捕获异常使图继续执行、如何让异常在wait_for_all()调用点被重新抛出、如何不借助异常而主动取消一张图、如何用graph::reset()让被取消的图重新可执行,以及如何控制嵌套并行与父图之间的取消联动关系。
说明:本文所述的 Flow Graph 位于 mold 仓库的 third-party/tbb 目录,是 mold 为链接过程并行化所捆绑的 oneTBB 代码库。其官方用户指南(tbb_userguide)存放在 third-party/tbb/doc/main/tbb_userguide 下,本指南即围绕其中的 Flow Graph 异常与取消专题展开。
一、异常与取消:Flow Graph 的两条执行中断路径
Flow Graph 的图执行可以被直接取消,也可以因为异常传播出节点 body而被取消;取消之后,你可以选择重置图以便重新执行。这是 Flow-Graph-exception-tips.rst 开篇给出的核心模型,整个专题由四个相互衔接的子主题构成:
| 子主题 | 文档 | 解决的核心问题 |
|---|---|---|
| 节点内捕获异常 | catching_exceptions.rst | 异常在节点 body 内部被捕获,图继续正常运行 |
| 显式取消图 | cancel_a_graph.rst | 不抛异常,通过task_group_context::cancel_group_execution()主动终止图 |
| 重置被取消的图 | use_graph_reset.rst | 取消后图处于不确定状态,用graph::reset()恢复可执行性 |
| 取消嵌套并行 | cancelling_nested_parallelism.rst | 图节点内嵌套的并行算法/子图是否随父图一起取消 |
在 oneTBB 的通用算法层面(如parallel_for),异常处理遵循三步流程(见 Exceptions_and_Cancellation.rst):捕获异常 → 取消算法(未开始的迭代不再执行)→ 在调用算法的线程上抛出异常。Flow Graph 的异常与取消语义正是这套通用机制在图结构上的具体化——异常不再只作用于一个算法,而是波及整张图的所有节点。
二、在抛出异常的节点内捕获异常:图继续执行
当节点 body 内部捕获并处理了异常时,图的执行不受任何影响,与普通函数调用中捕获异常的直觉完全一致。
2.1 未捕获异常导致整图取消
考虑如下三节点链式图:f1 → f2 → f3,其中f2的 body 直接抛出异常而未捕获:
graph g; function_node<int, int> f1(g, 1, [](int i) { return i; }); function_node<int, int> f2(g, 1, [](const int i) -> int { throw i; return i; }); function_node<int, int> f3(g, 1, [](int i) { return i; }); make_edge(f1, f2); make_edge(f2, f3); f1.try_put(1); f1.try_put(2); g.wait_for_all();这段代码的执行结果:
f2抛出的异常在传播出节点 body 时未被捕获,因此整张图所有节点的执行都被取消;- 异常在
g.wait_for_all()的调用点被重新抛出; - 由于该异常在
main中也没有被处理,程序将直接终止(terminate)。
从 _flow_graph_impl.h 的graph::wait_for_all()源码可以看出这套机制的底层实现:等待图空闲后,检查my_context->is_group_execution_cancelled()得到cancelled标志;若等待过程抛出异常,则在on_exception回调中将caught_exception与cancelled同时置为true,并在返回前执行my_context->reset()清理上下文状态。
2.2 在节点体内捕获:图不受影响
如果希望在节点内部消化异常、让图继续运转,把捕获逻辑写进 body 即可:
function_node<int, int> f2(g, 1, [](const int i) -> int { try { throw i; } catch (int j) { cout << "Caught " << j << "\n"; } return i; });此时f2对每个输入都正常返回,f1 → f2 → f3的整个数据流不受任何干扰——这就是"在抛出异常的节点内捕获"的推荐实践,适合异常属于节点局部业务、不影响全局执行语义的场景。
2.3 在wait_for_all()调用点捕获:图被取消
第三种选择是在调用wait_for_all()的地方捕获异常:
try { g.wait_for_all(); } catch (int j) { cout << "Caught " << j << "\n"; }这种情况下,图的执行已被取消。以 2.1 的图为例,具体后果是:
- 输入
1永远到不了f3(f2抛出后,下游f3不再执行); - 输入
2永远到不了f2和f3(取消后,尚未开始的节点不再处理任何消息)。
这正是文档强调的"异常传播出节点 body ⇒ 全部节点取消"语义,也为下一节的"图重置"埋下伏笔。
三、显式取消一张图:不抛异常的cancel_group_execution()
并非所有取消都源于异常。你可以通过显式task_group_context来主动取消图执行,从而避免"取消必须伴随异常"的限制。
3.1 外部线程显式取消
task_group_context t; graph g(t); function_node<int, int> f1(g, 1, [](int i) { return i; }); function_node<int, int> f2(g, 1, [](const int i) -> int { cout << "Begin " << i << "\n"; spin_for(0.2); cout << "End " << i << "\n"; return i; }); function_node<int, int> f3(g, 1, [](int i) { return i; }); make_edge(f1, f2); make_edge(f2, f3); f1.try_put(1); f1.try_put(2); spin_for(0.1); t.cancel_group_execution(); g.wait_for_all();关键行为(文档明确给出):
- 已经开始的节点会执行完毕:
f2会为输入1完整打印Begin 1与End 1,因为取消发生时它已在运行; - 尚未开始的节点不再启动:
f2不会收到输入2,更不会执行它。
这正是 Flow Graph 取消的"优雅停靠"特性——不打断正在进行的计算,只是拒绝启动新的计算。cancel_group_execution()的具体语义可参考 Cancellation_Without_An_Exception.rst:它会取消该task_group_context中的所有任务;方法返回true表示本次调用确实触发了取消,返回false表示该 context 此前已被取消。
3.2 从节点内部取消所属图
你可以在节点 body 内获取当前任务所属的task_group_context,并用它取消整张图:
graph g; function_node<int, int> f1(g, 1, [](int i) { return i; }); function_node<int, int> f2(g, 1, [](const int i) -> int { cout << "Begin " << i << "\n"; spin_for(0.2); cout << "End " << i << "\n"; task::self().group()->cancel_group_execution(); return i; }); function_node<int, int> f3(g, 1, [](int i) { return i; }); make_edge(f1, f2); make_edge(f2, f3); f1.try_put(1); f1.try_put(2); g.wait_for_all();task::self().group()返回当前正在执行任务的task_group_context*。文档特别强调:即使图在构造时没有显式传入task_group_context,也能从节点 body 中取到它——因为每个图在构造时都会创建自己的 context(默认是隔离上下文,详见第五节)。这一模式适合"发现无解条件即主动终止整张图"的自顶向下取消。
四、重置被取消的图:graph::reset()与reset_flags
4.1 为什么必须 reset?
无论取消来自未处理异常还是显式cancel_group_execution(),被取消的图及其节点都可能处于不确定状态(indeterminate state):
- 数据可能残留在节点缓冲区中(例如 3.1 例子中的输入
2); - 图执行过程中的各种优化可能让节点与边处于非初始状态。
因此,要想重新执行/重启一张图,必须先重置它。reset 的正确姿势是在捕获取消之后、重新投放消息之前调用:
try { g.wait_for_all(); } catch (int j) { cout << "Caught " << j << "\n"; // do something to fix the problem g.reset(); f1.try_put(1); f1.try_put(2); g.wait_for_all(); }这是一个完整的"异常 → 修复 → 重置 → 重跑"闭环:捕获异常后先修复导致问题的条件,再g.reset()清空不确定状态,随后重新try_put输入并再次wait_for_all(),让图以全新状态再执行一轮。
4.2 源码中的重置选项:reset_flags
从 _flow_graph_impl.h 可以看到graph::reset()支持的可选标志(可组合使用):
// flags to modify the behavior of the graph reset(). Can be combined. enum reset_flags { rf_reset_protocol = 0, rf_reset_bodies = 1 << 0, // delete the current node body, reset to a copy of the initial node body. rf_clear_edges = 1 << 1 // delete edges };| 标志 | 值 | 作用 |
|---|---|---|
rf_reset_protocol | 0 | 默认重置,仅恢复执行协议相关的内部状态 |
rf_reset_bodies | 1<<0 | 删除当前节点 body,恢复为初始 body 的副本 |
rf_clear_edges | 1<<1 | 删除图中所有边 |
此外,graph类还提供了配套的状态查询与取消接口(源码位置):
void reset(reset_flags f = rf_reset_protocol):线程不安全的状态重置,默认仅做协议级重置;void cancel():取消关联task_group_context的执行;bool is_cancelled():查询图是否处于已取消状态;bool exception_thrown():查询图是否因异常而中止。
wait_for_all()每次调用时也会先清零cancelled与caught_exception标志(见 源码),这意味着图对象本身是可以反复wait_for_all → 捕获/取消 → reset → 再 wait_for_all复用的,这正是长时间运行的服务中处理"单轮图执行失败"的标准模式。
五、取消嵌套并行:task_group_context的绑定关系决定一切
Flow Graph 的节点 body 内常常会调用其他并行算法(parallel_for、parallel_reduce等)或嵌套的 Flow Graph。当外层图被取消(显式或异常触发)时,这些嵌套并行是否也被取消?cancelling_nested_parallelism.rst 给出的结论很简洁:
嵌套并行是否被取消,取决于内层 context 是否绑定到外层 context。
具体规则:
- 若嵌套算法/子图显式使用了与外层图相同的
task_group_context(或绑定到其上的 context),则嵌套并行会随之被取消; - 若嵌套并行使用独立/隔离的 context,则不受外层取消影响,会继续执行完毕;
- 默认行为:如果你没有为一张 Flow Graph 显式提供
task_group_context,它会被创建为隔离上下文(isolated context)——这正是 _flow_graph_impl.h 中两个构造函数注释所印证的设计:
//! Constructs a graph with isolated task_group_context graph(); //! Constructs a graph with use_this_context as context explicit graph(task_group_context& use_this_context);因此,若要实现"父图取消 ⇒ 嵌套任务一并取消",必须显式构造共享的task_group_context并同时传入外层图与内层并行结构(即 3.1 节graph g(t)的写法)。若希望子任务独立于父图生命周期(例如某些清理型任务必须跑完),则应让内层使用自己的隔离 context。
这一机制与 oneTBB 通用嵌套并行规则完全一致(参见 Cancellation_and_Nested_Parallelism.rst):所有嵌套并行的取消关系都由显式task_group_context对象控制,未显式提供 context 的嵌套算法默认处于隔离状态。
六、实践要点与决策速查
综合文档与源码,可以归纳出 Flow Graph 异常处理与取消的决策模型:
- 异常是节点局部业务问题→ 在 body 内
try/catch,图不受影响,数据流继续; - 异常需要中止整张图→ 让异常传播出 body,在
wait_for_all()处捕获;注意此时图已被取消、缓冲区可能有残留输入; - 需要无异常地中止整张图→ 显式
task_group_context+cancel_group_execution(),或从节点内task::self().group()->cancel_group_execution();已开始的节点跑完,未开始的节点不再启动; - 需要复用被取消的图→ 捕获/处理取消后调用
g.reset()(必要时配合rf_reset_bodies、rf_clear_edges),再重新try_put输入; - 需要控制嵌套并行是否随父图取消→ 显式共享
task_group_context则联动取消,隔离 context 则各自独立,默认创建隔离 context。
这套机制在 mold 的 third-party/tbb 代码树中均有完整实现与文档支撑:用户指南位于 third-party/tbb/doc/main/tbb_userguide(核心入口 Flow-Graph-exception-tips.rst),底层实现位于 _flow_graph_impl.h 的graph类与reset_flags枚举。掌握上述决策点,即可在依赖 Flow Graph 构建流水线的场景中,写出"异常可控、取消可预期、图可复用"的健壮并行代码。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考