ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

oneTBB Flow Graph 拓扑管理指南:正确使用 make_edge 与 remove_edge 构建和拆除数据流边

oneTBB Flow Graph 拓扑管理指南:正确使用 make_edge 与 remove_edge 构建和拆除数据流边 并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载在 oneTBB Flow Graph 中节点之间通过**有向边edge**传递消息边的构建与拆除决定了整个图的拓扑结构。本文围绕 oneTBB 用户指南中的 use_make_edge 主题系统讲解流图的建边/拆边约定为什么应统一使用flow::make_edge与flow::remove_edge而避免直接调用register_successor、register_predecessor、remove_successor、remove_predecessor这四类节点底层函数并结合 flow_graph.h 源码剖析二者在内部的真实调用链最后给出可复制运行的实战示例。读完本文你将掌握 oneTBB Flow Graph 中拓扑管理的规范用法并能安全地在运行期动态调整图结构。一、核心约定对外只用 make_edge / remove_edgeoneTBB Flow Graph 的官方指南对边的创建与移除给出了三条基本准则见 use_make_edge.rst使用make_edge和remove_edge来建边与拆边避免使用register_successor和register_predecessor避免使用remove_successor和remove_predecessor。作为一条明确的约定convention当需要表达图的拓扑结构时只使用flow::make_edge与flow::remove_edge这两个函数。运行时库runtime library内部确实是通过节点函数例如senderT::register_successor来创建这些边的但这些节点函数不应该被用户直接调用。运行时库之所以直接调用这些节点级函数是为了在运行期对拓扑执行内部优化例如避免不必要的前驱/后继登记、处理预约与并发竞态等。换句话说make_edge/remove_edge是面向用户的稳定对外接口而register_*/remove_*是面向运行时库内部的实现接口。用户代码与运行时库各司其职才能在拓扑变更时保持语义一致。二、接口全貌make_edge / remove_edge 的重载与底层实现在 include/oneapi/tbb/flow_graph.h 中make_edge与remove_edge通过命名空间oneapi::tbb::flow::v1中的using detail::d2::make_edge;/using detail::d2::remove_edge;flow_graph.h#L3357-L3358暴露给用户最终入口位于oneapi::tbb::flow。2.1 基本形式单个发送者到单个接收者最基础的签名是将一个senderT与一个receiverT相连template typename T inline void make_edge( senderT p, receiverT s ); template typename T inline void remove_edge( senderT p, receiverT s );其中p是前驱predecessor消息的发送方s是后继successor消息的接收方。边的方向就是消息流动的方向从p到s。2.2 多端口节点自动对接 0 号端口很多节点如join_node、indexer_node、composite_node、async_node等拥有多个输入/输出端口。make_edge/remove_edge为此提供了三组便捷重载flow_graph.h#L2572-L2591 与 flow_graph.h#L2605-L2623// 多输出节点 → 多输入节点对接双方各自的 0 号端口 template typename T, typename V, typename typename T::output_ports_type, typename typename V::input_ports_type inline void make_edge( T output, V input ); // 多输出节点 → 单接收者取多输出节点的 0 号端口 template typename T, typename R, typename typename T::output_ports_type inline void make_edge( T output, receiverR input ); // 单发送者 → 多输入节点取多输入节点的 0 号端口 template typename S, typename V, typename typename V::input_ports_type inline void make_edge( senderS output, V input );remove_edge提供完全对称的三组重载。这些重载最终都通过std::get0(node.output_ports())/std::get0(node.input_ports())取到端口引用再递归调用基本形式完成建边。这意味着直接传节点对象给make_edge时默认连接的是第 0 个端口要连接其他端口请显式使用input_portk(node)/output_portk(node)获取端口引用可参考 Flow_Graph_Reservation.rst 中make_edge(buf1, input_port0(jn))的用法。2.3 底层调用链make_edge 内部做了什么从源码可以看到make_edge的真实执行路径flow_graph.h#L2560-L2570template typename T inline void internal_make_edge( senderT p, receiverT s ) { register_successor(p, s); // 将后继 s 登记到发送者 p 的成功者列表 fgt_make_edge( p, s ); // 向流图工具ITT/Flow Graph Tools发出建边事件 } template typename T inline void make_edge( senderT p, receiverT s ) { internal_make_edge( p, s ); }remove_edge的路径完全对称flow_graph.h#L2593-L2603template typename T inline void internal_remove_edge( senderT p, receiverT s ) { remove_successor( p, s ); // 从发送者 p 的成功者列表中移除后继 s fgt_remove_edge( p, s ); // 向流图工具发出拆边事件 }可见make_edge/remove_edge并非简单转发而是做了两件事拓扑登记调用自由函数register_successor(senderC, receiverC)/remove_successor(senderC, receiverC)flow_graph.h#L270-L278它们分别包装了senderT的受保护虚函数register_successor(successor_type r)与remove_successor(successor_type r)flow_graph.h#L257-L261工具插桩发出fgt_make_edge/fgt_remove_edge事件供 oneTBB 的流图追踪工具flow graph tools在调试与性能分析时还原拓扑演化。这也从实现层面解释了文档的建议用户直接调用senderT::register_successor等底层函数不仅会绕过fgt_*插桩导致工具看不到这条边而且这些方法在senderT/receiverT中本就是protected的——它们依赖friend自由函数间接调用属于运行时库内部契约不应被用户代码触碰。三、为什么推荐 make_edge / remove_edge而不是 register_* / remove_*结合源码结构可以归纳出四条理由前两条为文档明文约定后两条可从源码结构推断接口稳定性make_edge/remove_edge是 oneTBB 对外承诺的稳定 API而register_*/remove_*是为运行时优化保留的实现细节可能随版本演进变化语义完整性make_edge/remove_edge除了登记/移除拓扑关系还负责流图工具的插桩事件保证工具视角与真实拓扑一致绕过它们会让调试与剖析失真双向一致性从实现看部分节点如function_node在remove_successor内部还会调用tbb::detail::d2::remove_predecessor(r, *this)来同步清理接收方的前驱列表flow_graph.h#L2313-L2317。直接操作单侧 API 容易破坏这种前驱/后继的双向一致性并发安全运行时库在处理预约reservation、限流等场景时对登记顺序有专门设计例如 flow_graph.h#L3127-L3145 中为打破节点预约与 register_successor 之间死循环而引入的register_predecessor_task。绕开公共 API 自行建边很可能触发文档 avoiding_data_races.rst 所警示的数据竞争问题。四、实战示例 1用 make_edge 串联两个 function_nodeEdges.rst 给出了最典型的建边示例用make_edge把一个输出平方的节点接到一个打印节点上让运行时库自动在节点间搬运消息using namespace oneapi::tbb::flow; graph g; function_node int, int n( g, unlimited, []( int v ) - int { cout v; spin_for( v ); cout v; return v; } ); function_node int, int m( g, 1, []( int v ) - int { v * v; cout v; spin_for( v ); cout v; return v; } ); make_edge( n, m ); // 创建从 n 到 m 的边 n.try_put( 1 ); n.try_put( 2 ); n.try_put( 3 ); g.wait_for_all();这段代码的关键点n以unlimited并发创建其多次调用可以并行执行m的并发限制为1因此它的调用会被串行化同一个时刻只有一个 body 在执行。make_edge(n, m)建立从n到m的边之后n每次返回的值v都会被运行时库自动传递给m——用户无需手动把结果放进m。这正是 Flow Graph 消息传递协议的核心边就是消息的输送管道。消息的流动完全异步n.try_put(...)只负责把值放入图内并触发任务最终通过g.wait_for_all()等待所有相关任务完成。五、实战示例 2remove_edge 动态改拓扑单推 vs 广播Flow_Graph_Single_Vs_Broadcast.rst 展示了边与节点推送策略的配合以及用remove_edge在运行期拆边、重组的典型手法。它演示了两种推送策略single-push单推无论节点有多少个后继每条消息只推给一个后继。只有被设计为缓冲并转发的节点如buffer_node、queue_node采用该策略broadcast-push广播消息被推给所有以 push 模式相连且接受该消息的后继。除缓冲类节点外其余节点如broadcast_node、function_node默认都是广播策略。示例中先用make_edge把buffer_node同时接到三个function_node上make_edge(buf1, f1); make_edge(buf1, f2); make_edge(buf1, f3); buf1.try_put(continue_msg()); buf1.try_put(continue_msg()); buf1.try_put(continue_msg()); g.wait_for_all(); // 输出: after single-push test, g_cnt 3, b13, b20, b30由于buffer_node是单推策略3 条消息全部只推给了第一个function_nodeb13b2、b3均为 0。随后用remove_edge拆掉这三条边改用broadcast_node重新建边remove_edge(buf1, f1); remove_edge(buf1, f2); remove_edge(buf1, f3); broadcast_nodecontinue_msg bn(g); make_edge(bn, f1); make_edge(bn, f2); make_edge(bn, f3); bn.try_put(continue_msg()); bn.try_put(continue_msg()); bn.try_put(continue_msg()); g.wait_for_all(); // 输出: after broadcast-push test, g_cnt 9, b13, b23, b33broadcast_node把 3 条消息各推给 3 个后继共产生 9 次推送g_cnt 9三个 body 各执行 3 次。这个例子说明remove_edge与make_edge可以在同一图中反复组合实现运行期拓扑的动态调整——先拆旧边、再建新边是重组图的推荐方式。六、依赖图中的边边的语义影响节点行为边不仅是消息通道还会影响节点的调度语义。在 Dependence_Graph.rst 介绍的依赖图dependence graph中所有节点都是continue_nodecontinue_msg节点之间传递continue_msg而边定义了各计算的偏序关系。与一般数据流图不同依赖图中的continue_node不会为每条消息都生成一个任务而是记录自己的前驱数量只有收到的消息数达到前驱总数时才触发 body 执行。这一点在源码中得到印证continue_receiver构造时即保存my_predecessor_count my_initial_predecessor_count number_of_predecessorsflow_graph.h#L364-L369。因此每条make_edge连入的边都会改变节点的前驱计数从而改变它的触发条件。以三明治制作式依赖图为例typedef continue_node continue_msg node_t; typedef const continue_msg msg_t; oneapi::tbb::flow::graph g; node_t A(g, [](msg_t){ a(); } ); node_t B(g, [](msg_t){ b(); } ); node_t C(g, [](msg_t){ c(); } ); node_t D(g, [](msg_t){ d(); } ); node_t E(g, [](msg_t){ e(); } ); node_t F(g, [](msg_t){ f(); } ); make_edge(A, B); make_edge(B, C); make_edge(B, D); make_edge(A, E); make_edge(E, D); make_edge(E, F); A.try_put( continue_msg() ); g.wait_for_all();节点D有B和E两个前驱两条入边所以它必须同时收到 B 与 E 各自发出的continue_msg才会开始执行节点C、F各只有一个前驱只要对应的前驱完成即可执行全部执行是异步的A.try_put迅速返回只负责计数与派生任务真正阻塞的只有g.wait_for_all()且等待线程仍可参与 oneTBB 工作池中的其他任务执行。这个例子提醒我们在用make_edge布线时每一条边都携带语义——在依赖图中它决定谁必须等待谁在数据流图中它决定消息流向哪里。而错误使用register_predecessor等底层接口很容易造成计数不一致导致节点永远不触发或提前触发。七、最佳实践小结综合文档约定与源码实现使用make_edge/remove_edge时应遵循以下要点场景推荐做法避免做法创建边flow::make_edge(p, s)或make_edge(output_node, input_node)默认 0 号端口直接调用register_successor/register_predecessor移除边flow::remove_edge(p, s)直接调用remove_successor/remove_predecessor连接指定端口用input_portk(node)/output_portk(node)取得端口后传给make_edge依赖默认端口而不核对端口下标运行期重组拓扑先remove_edge拆旧边再make_edge建新边在消息流经期间随意增删边而不同步等待参见 avoiding_data_races.rst更多的建边话题何时用广播、如何与节点通信、输入节点的使用、数据竞争规避等收录在 Flow_Graph_making_edges_tips.rst 这一主题目录下其中 broadcast_or_send.rst 进一步讨论了单播与广播的选择communicate_with_nodes.rst 讲解了try_put等节点通信手段可与本文配合阅读。相关实现可深入研读 include/oneapi/tbb/flow_graph.h 中senderT、receiverT、internal_make_edge/internal_remove_edge及各类节点对register_successor/remove_successor的覆写以理解每条边背后运行时库所做的登记、同步与优化工作。赞分享并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载相关推荐oneTBB Flow Graph 动态节点移除的风险规避指南安全构建与修改图的拓扑oneTBB Flow Graph 动态节点移除的风险规避指南安全构建与修改图的拓扑 oneTBBoneAPI Threading Building Blo并发编程高性能计算oneTBB 图并行编程指南用 Flow Graph 构建数据流图与依赖图oneTBB 图并行编程指南用 Flow Graph 构建数据流图与依赖图 本篇技术指南围绕 oneAPI Threading Building Blocks并发编程高性能计算oneTBB Flow Graph 中 wait_for_all() 的正确用法防止悬空节点与未定义行为oneTBB Flow Graph 中 wait_for_all 的正确用法防止悬空节点与未定义行为 graph::wait_for_all 是 oneTBB并发编程高性能计算上一篇揭秘Windows上的革命性Android应用安装体验APK Installer深度解析下一篇Mac Mouse Fix 完全指南:3步把普通鼠标改成比触控板更好用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表