TBB Flow Graph 消息推送策略深度解析Single-push 与 Broadcast-push 完整指南【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文基于本仓库mold内置的 oneAPI Threading Building BlocksTBB第三方库文档与源码系统讲解 Flow Graph 中节点间消息传递的两大推送策略single-push单播推送与broadcast-push广播推送。通过完整代码示例、输出结果分析以及 flow_graph.h 源码级佐证你将掌握 buffer_node 与 broadcast_node 的行为差异、缓冲节点为何必须采用 single-push以及如何在真实图结构中正确选择节点类型避免期待广播却只收到单播的经典陷阱。一、背景Flow Graph 的消息通信模型在 oneAPI TBB Flow Graph 中节点node之间通过**消息message**进行通信以此传递数据并强制执行依赖关系。节点是继承自oneapi::tbb::flow::graph_node的类通常还继承senderT发送者、receiverT接收者或两者兼有详见 Nodes 文档。消息的传递依赖两类操作push推上游节点主动将消息推送给下游后继节点通过边的 push 模式完成pull拉下游后继节点在空闲时主动向有消息的前驱节点拉取消息。消息被推送给后继者时采用何种策略取决于节点的类型。正是这个差异决定了buffer_node与broadcast_node在相同图结构下的行为截然不同——这正是本文要聚焦的核心问题。二、两种推送策略的定义与对比根据 Flow_Graph_Single_Vs_Broadcast.rst无论节点存在多少能够接收消息的后继者其推送消息时只有两种策略策略定义代表节点single-push单播推送无论存在多少能接收消息的后继节点每条消息只发送给其中一个后继者。buffer_node、queue_node、priority_queue_node、sequencer_nodebroadcast-push广播推送消息会被推送给每一个通过 push 模式边与之相连、且能接收该消息的后继节点。broadcast_node及其他绝大多数节点两种策略的适用面并不对等文档给出了明确结论只有被设计用来缓冲持有并转发收到的消息的节点才采用 single-push 策略其余所有节点一律采用 broadcast-push 策略。这一结论的源码依据可以在 flow_graph.h 中找到broadcast_node第 1211 行内部使用broadcast_cache第 1218 行管理后继者其try_put_task_impl直接把消息交给my_successors.try_put_task(...)即向所有后继者广播buffer_node第 1284 行内部使用round_robin_cache第 1300 行管理后继者。从源码结构可以推断round-robin轮询式的后继者缓存正是每条消息只挑选一个后继者转发的实现机制这也与 single-push 语义吻合同时queue_node第 1697 行、sequencer_node第 1803 行、priority_queue_node第 1879 行分别继承自buffer_node/queue_node因此它们天然继承了 single-push 行为。三、完整示例用一份代码同时验证两种策略原文档给出了一段可直接编译运行的对照实验代码用同一个fn_body1函数体构造三个function_node先以buffer_node做 single-push 测试再以broadcast_node做 broadcast-push 测试通过原子计数器统计每个节点实际被调用的次数。using namespace oneapi::tbb::flow; std::atomicsize_t g_cnt; struct fn_body1 { std::atomicsize_t body_cnt; fn_body1(std::atomicsize_t b_cnt) : body_cnt(b_cnt) {} continue_msg operator()( continue_msg /*dont_care*/) { g_cnt; body_cnt; return continue_msg(); } }; void run_example1() { // example for Flow_Graph_Single_Vs_Broadcast graph g; std::atomicsize_t b1; // local counts std::atomicsize_t b2; // for each function_node body std::atomicsize_t b3; function_nodecontinue_msg f1(g, serial, fn_body1(b1)); function_nodecontinue_msg f2(g, serial, fn_body1(b2)); function_nodecontinue_msg f3(g, serial, fn_body1(b3)); buffer_nodecontinue_msg buf1(g); // // single-push policy // g_cnt b1 b2 b3 0; 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(); printf(after single-push test, g_cnt %d, b1%d, b2%d, b3%d\n, (int)g_cnt, (int)b1, (int)b2, (int)b3); remove_edge(buf1, f1); remove_edge(buf1, f2); remove_edge(buf1, f3); // // broadcast-push policy // broadcast_nodecontinue_msg bn(g); g_cnt b1 b2 b3 0; 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(); printf(after broadcast-push test, g_cnt %d, b1%d, b2%d, b3%d\n, (int)g_cnt, (int)b1, (int)b2, (int)b3); }3.1 代码要点解读function_nodecontinue_msg以continue_msg流图中的继续信号类型作为输入输出serial表示串行并发度等价于并发上限为 1即同一时刻最多执行一次函数体详见 Nodes 文档 中关于concurrency参数的说明make_edge/remove_edge分别用于建立与拆除节点间的有向边push 模式默认生效try_put把消息放入节点的输入端口g.wait_for_all()阻塞直到图中所有活动任务完成保证计数输出前消息全部处理完毕三个fn_body1分别持有独立的原子计数器b1/b2/b3全局计数器g_cnt记录所有函数体被调用的总次数从而精确区分哪个节点收到了消息。3.2 运行输出与分析after single-push test, g_cnt 3, b13, b20, b30 after broadcast-push test, g_cnt 9, b13, b23, b33single-push 测试结果解读buffer_node采用 single-push 策略转发消息。向buf1放入三条消息最终总共只推送出三条消息g_cnt 3并且三条全部被第一个function_nodef1接收b2、b3均为 0。需要特别强调的是当存在多个可接收消息的后继者时库本身并没有规定必须推送给哪一个——本示例中恰好全部给了f1这是轮询调度下的一个可能结果不应假设总是发给第一个后继者。broadcast-push 测试结果解读broadcast_node会把收到的每条消息推送给所有能接收它的后继者。三条消息 × 三个function_node 共推送九条消息g_cnt 9每个函数节点各收到三条b1 b2 b3 3。四、为什么缓冲节点必须是 single-pushbuffering 机制分析要理解只有缓冲节点采用 single-push需要结合 Flow_Graph_Buffering_in_Nodes.rst 中描述的缓冲语义当节点无法把消息成功推送给任何后继者时有两种可能的结果节点保存消息留待以后转发缓冲行为节点丢弃消息非缓冲行为。如果节点会丢弃无法转发的消息而用户不希望丢弃就应当把它连接到一个能保存消息的缓冲节点上游。消息被节点保存后有两种方式被传递出去后继者通过try_get()或try_reserve()拉取消息——成功拉取后该消息会从存储它的节点中移除用make_edge()连接新的后继者——节点会尝试把已保存的消息推送给这个新后继者。缓冲节点如buffer_node承担暂存消息、等待下游消费的职责其核心是消息的有序、一次消费语义。如果它采用广播策略就必须解决一系列棘手的语义问题——broadcast_or_send.rst 用一个priority_queue_node的例子做了透彻推演假设priority_queue_node中只有一条消息 9后继节点f1接受了 9而另一个后继f2拒绝了它。随后优先级更高的 100 到达且此时f2空闲可接收消息——那么f2应该收到 9 还是优先级更高的 100要保证所有后继者都收到每条消息会引入垃圾回收问题消息需要为每个后继者各保存一份副本并极大复杂化推理。因此这些缓冲节点把每条消息只推送给一个后继者从而保证每个缓冲消息只被消费一次且可借助轮询在多个消费者之间分摊负载。这一设计在源码中同样有迹可循buffer_node类声明flow_graph.h 第 1284-1300 行同时继承了reservable_item_buffer可预留的消息缓冲、receiverT与senderT并使用round_robin_cache作为后继者集合其单播语义与缓冲 单一后继转发的角色定位完全一致。五、实战扩展广播 单播组合出带优先级的广播效果既然缓冲节点只能把每条消息发给一个后继者如果确实需要所有消费者都按优先级收到全部消息该怎么办broadcast_or_send.rst 给出了标准解法为每个消费者各建一个priority_queue_node再用一个broadcast_node把消息同时推给所有队列。graph g; function_nodeint, int, rejecting f1(g, 1, [](int i) - int { spin_for(0.1); cout f1 consuming i \n; return i; }); function_nodeint, int, rejecting f2(g, 1, [](int i) - int { spin_for(0.2); cout f2 consuming i \n; return i; }); priority_queue_nodeint q1(g); priority_queue_nodeint q2(g); broadcast_nodeint b(g); make_edge(b, q1); make_edge(b, q2); make_edge(q1, f1); make_edge(q2, f2); for (int i 10; i 0; --i) { b.try_put(i); } g.wait_for_all();这个示例还有两个值得注意的细节rejecting缓冲策略function_node默认会在输入端口缓冲收到的消息。为了让priority_queue_node正常工作这里把f1、f2的缓冲策略显式设为rejecting拒绝缓冲使它们不内部缓存消息而是完全依赖上游priority_queue_node的缓冲——避免同一份消息在两级缓冲中重复排队。单播的天然用途在只用一个priority_queue_node连接多个消费者时前文use_buffer_and_two_nodes示例每条消息只会被f1或f2之一消费消息按优先级顺序从队列弹出形成优先级队列 多消费者负载均衡的结构——这正是 single-push 的典型实战价值。所以连接一个节点到多个后继者之前务必先确认该节点的输出是广播给所有后继者还是只发给其中一个再据此设计图结构。六、底层原理动态 Push/Pull 协议如何驱动两种推送策略single-push 与 broadcast-push 并非孤立存在它们运行在 Flow Graph 的动态 Push/Pull 协议之上详见 Flow_Graph_Message_Passing_Protocol.rst节点可能暂时无法接收并处理来自前驱的消息。为了让图高效运行此时边会从 push 模式切换为 pull 模式——当下游有能力处理消息时它可以主动查询前驱是否有可用消息。若不切换前驱只能反复尝试推送直到后继接收白白消耗资源。边处于 pull 模式时后继者在空闲时会尝试从某个前驱拉取消息若前驱有消息后继者处理它边保持 pull 模式若前驱没有消息边切换回 push 模式。上图源自 Flow_Graph_Message_Passing_Protocol.rst位于 Images 目录展示了这一动态协议边在 push/pull 两种状态之间随消息供给与消费能力的变化而切换。理解这一点有助于解释本文示例中的行为buffer_node在多个后继者都处于接收状态时以 single-push 策略把每条缓冲消息轮询交给其中一个后继者正是协议中 push 侧转发与 pull 侧消费协同的结果。七、实践要点与选型速查基于以上分析在实际构建 Flow Graph 时可遵循以下要点识别节点推送策略凡是缓冲类节点buffer_node、queue_node、priority_queue_node、sequencer_node均为 single-push其余节点含broadcast_node、function_node等均为 broadcast-push。一对多不等于广播把一个缓冲节点连接到多个后继者消息仍只会流向其中一个轮询选择具体是谁不做保证要实现真正的一对多广播请显式插入broadcast_node。多消费者 优先级需要所有消费者都收到全部消息且按优先级处理时采用broadcast_node 每消费者一个priority_queue_node的组合。配合rejecting策略将function_node等节点的缓冲策略设为rejecting可避免上游缓冲节点与下游节点内部缓冲之间的消息双重排队确保缓冲节点正常工作。善用等待与边管理测试或使用中通过g.wait_for_all()确保消息全部处理完毕make_edge/remove_edge用于动态调整边的连接关系。理解底层的动态协议边的 push/pull 状态自动切换见消息传递协议文档这保证了 single-push/broadcast-push 在并发环境下的正确性与效率。八、延伸阅读本文聚焦的 single-push / broadcast-push 是 Flow Graph 基础概念Basic Flow Graph Concepts 目录的一部分与本主题直接相关的仓库文档还包括Flow Graph 缓冲与转发消息存储/丢弃、try_get()/try_reserve()拉取语义发送给一个或多个后继者single-push 节点清单与优先级队列实战示例消息传递协议动态 Push/Pull 状态机Flow Graph 提示建边、嵌套并行与资源使用等进阶建议Flow Graph 预留机制try_reserve与预留/释放协议。上述文档对应的完整实现位于 include/oneapi/tbb/flow_graph.h其中broadcast_node第 1211 行与buffer_node第 1284 行的后继者缓存实现broadcast_cache与round_robin_cache是理解两种推送策略最直接的代码依据。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?