mold 仓库内置 TBB input_node 使用指南:激活时机、消息丢失规避与源码级原理解析

mold 仓库内置 TBB input_node 使用指南:激活时机、消息丢失规避与源码级原理解析 mold 仓库内置 TBB input_node 使用指南激活时机、消息丢失规避与源码级原理解析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文聚焦 oneTBBoneAPI Threading Building Blocks流图flow graph中的input_node节点围绕“何时激活节点才能保证消息不丢失”这一核心问题展开。全文以本仓库内 third-party/tbb/doc/main/tbb_userguide/use_input_node.rst 为主干结合 Data_Flow_Graph.rst 中的完整示例与 flow_graph.h 的源码实现进行纵深剖析。读完本文你将掌握input_node的构造与激活语义、flow_control停止机制、避免消息丢失的两种安全接线策略以及input_node在内存受限复杂流图中的背压backpressure价值。说明mold 是一个高速链接器其仓库将 oneTBB 作为第三方依赖整体内置见 third-party/tbb本文讲解的即为该依赖自带的 TBB 用户指南文档及其对应源码。图示使用与 Data_Flow_Graph 章节配套的官方示意图 flow_graph.jpg。一、input_node 是什么流图中唯一“只发不收”的源节点在数据流图data flow graph中节点是发送和接收数据消息的计算单元。绝大多数节点既收又发而input_node是一个特例它只发送消息、不接收任何输入消息。正如 Data_Flow_Graph.rst 所述Aninput_node, as the name implies only sends messages and does not receive messages.从源码可以精确验证这一点。flow_graph.h 中的类声明template typename Output __TBB_requires(std::copyableOutput) class input_node : public graph_node, public sender Output { public: typedef Output output_type; typedef typename senderoutput_type::successor_type successor_type; // Input node has no input type typedef null_type input_type; ...要点input_nodeOutput继承senderOutput通过register_successor/remove_successor管理下游节点其消息类型为Outputinput_type被定义为null_type明确表达“没有输入”这一语义Output必须满足std::copyable约束__TBB_requires(std::copyableOutput)即输出类型必须可拷贝才能被安全地传递给下游节点。它的典型使用场景是替代“显式循环向图内try_put消息”的写法把数据生成逻辑封装进图的执行流中详见第四节。二、构造即“非激活”默认状态与激活 API2.1 构造签名input_node的构造函数为template typename Body input_node( graph g, Body body, bool is_activetrue )从 flow_graph.h 的源码可以看出构造时内部状态被显式初始化为非激活template typename Body __TBB_requires(input_node_bodyBody, Output) __TBB_NOINLINE_SYM input_node( graph g, Body body ) : graph_node(g), my_active(false) , my_body( new input_body_leaf output_type, Body(body) ) , my_init_body( new input_body_leaf output_type, Body(body) ) , my_successors(this), my_reserved(false), my_has_cached_item(false)构造函数要点my_active(false)无论调用者是否传第三个参数构造出的input_node都处于**非激活inactive**状态两个 bodymy_body是当前生效的 bodymy_init_body保存初始 body 的副本供reset_node恢复初始状态时克隆使用my_has_cached_item(false)与my_reserved(false)初始无缓存消息、无保留消息。2.2 激活机制与源码验证激活一个处于非激活状态的节点需要调用其成员函数activate()input_node int src( g, src_body(10), false ); // use it in calls to make_edge… src.activate();flow_graph.h 中activate()的实现//! Activates a node that was created in the inactive state void activate() { spin_mutex::scoped_lock lock(my_mutex); my_active true; if (!my_successors.empty()) spawn_put(); }可以看到activate()在加锁置位my_active true后只要当前已有后继节点!my_successors.empty()就会立即调用spawn_put()调度一个任务开始产出消息。反过来register_successorflow_graph.h在节点已处于激活状态时也会触发spawn_put()bool register_successor( successor_type r ) override { spin_mutex::scoped_lock lock(my_mutex); my_successors.register_successor(r); if ( my_active ) spawn_put(); return true; }这正是“激活时机”会成为正确性问题根源的底层原因边edge的建立本身就可能触发消息生产。若在连边之前节点已激活那么连第一条边时消息就可能开始流动。三、为什么“激活时机”会导致消息丢失问题剖析3.1 过早激活的问题场景以use_input_node.rst中的示例为例make_edge( squarer, summer ); make_edge( cuber, summer ); input_node int src( g, src_body(10), false ); make_edge( src, squarer ); make_edge( src, cuber ); src.activate(); g.wait_for_all();若input_node在一开始就被置为激活状态会发生什么文档明确指出if theinput_nodewas toggled to the active state at the beginning, it might send a message to squarer immediately after the edge to squarer is connected. Later, when the edge to cuber is connected, cuber will receive all future messages, but may have already missed some.即节点一旦激活make_edge(src, squarer)建立的那一刻src就可能立刻向squarer发送消息而当稍后再建立make_edge(src, cuber)时cuber已经错过了此前发出的那批消息只能收到未来的消息。结果是同一份数据没有均匀流向两个后继导致计算不完整例如求和结果缺少立方分支的早期数据。从源码角度这就是 2.2 节中register_successor内部if (my_active) spawn_put()的直接后果只要节点激活每一次注册后继都会立即触发消息生产而生产出来的消息只会广播给“当前已注册”的后继。3.2 安全的一般性做法文档给出的通用建议是In general it is safest to create yourinput_nodeobjects in the inactive state and then activate them after the whole graph is constructed.也就是以非激活状态构造input_node第三个参数传false或接受默认构造行为——如源码所示构造后即处于非激活态用make_edge建立全部边全部建边完成后调用activate()最后调用g.wait_for_all()等待整张图执行完毕。代价则是这种方式把“图构建”和“图执行”串行化了construct-then-execute无法让构建与执行重叠以提升启动阶段吞吐。四、进阶允许“边建边跑”的 DAG 安全模式4.1 适用条件文档指出在某些条件下可以安全地在构建图的同时执行input_node让构建与执行重叠overlap of construction and execution图是有向无环图DAG每个input_node只有一个后继。此时只需按**逆拓扑序reverse topological order**建边先建“深度最大”的边再逐层回退到最浅的边。这样即使src在建完边后立即被激活也不会丢消息。4.2 完整示例文档给出的示例略作整理保留原文语义const int limit 10; int count 0; graph g; oneapi::tbb::flow::input_nodeint src( g, - int { if ( count limit ) { return count; } fc.stop(); return {}; }); src.activate(); oneapi::tbb::flow::function_nodeint,int func1( g, 1, []( int i ) - int { std::cout i \n; return i; } ); oneapi::tbb::flow::function_nodeint,int func2( g, 1, []( int i ) - int { std::cout i \n; return i; } ); make_edge( func1, func2 ); make_edge( src, func1 ); g.wait_for_all();这段代码之所以安全原因有二建边顺序正确先建func1 - func2这条深层的边再建src - func1。若反过来先建src - func1func1可能在func2挂接之前就产生消息该消息将被丢弃此时func1尚无后继可转发src只有单一后继若src有多个后继先挂接的后继可能收到消息而后挂接的后继收不到。单一后继则不存在这种“后继分叉导致的漏发”。注意该示例中src在func1、func2尚未构造、尚未建边时就调用了activate()但由于此刻src尚无后继activate()中的if (!my_successors.empty()) spawn_put()条件不成立不会立即产出消息真正开始产出是在make_edge(src, func1)完成之后。这正是 DAG 单后继 逆拓扑序建边模式下“边建边跑”不会丢消息的原理所在。五、消息如何停止flow_control 与 body 约定5.1 body 的函数签名input_node的构造参数body是一个函数对象或 lambda必须提供如下形式的函数调用运算符Output Body::operator()( oneapi::tbb::flow_control fc );运行时库会反复调用该函数运算符直到 body 内部调用fc.stop()为止。这一约定在 Data_Flow_Graph.rst 中以src_body类完整演示class src_body { const int my_limit; int my_next_value; public: src_body(int l) : my_limit(l), my_next_value(1) {} int operator()( oneapi::tbb::flow_control fc ) { if ( my_next_value my_limit ) { return my_next_value; } else { fc.stop(); return int(); } } };可见一个标准的源 body 逻辑是还有数据可产时返回下一个值数据耗尽时调用fc.stop()并返回一个默认构造值该值不会被实际发送。5.2 flow_control 的源码实现flow_control定义于 third-party/tbb/include/oneapi/tbb/detail/_pipeline_filters.h//! input_filter control to signal end-of-input for parallel_pipeline class flow_control { bool is_pipeline_stopped false; flow_control() default; templatetypename Body, typename InputType, typename OutputType friend class concrete_filter; templatetypename Output __TBB_requires(std::copyableOutput) friend class d2::input_node; public: void stop() { is_pipeline_stopped true; } };源码要点flow_control的默认构造函数是私有的只有input_node以及parallel_pipeline的 filter能创建它——用户代码只能接收由框架传入的fc引用stop()把内部标志is_pipeline_stopped置为true在input_node的try_reserve_apply_bodyflow_graph.h中框架以d1::flow_control control;构造控制对象并执行 body随后检查my_has_cached_item !control.is_pipeline_stopped;——即调用fc.stop()后产出的“返回默认值”不会被缓存、不会发给下游节点停止生产。因此对用户而言flow_control只暴露一个职责在数据耗尽时调用fc.stop()通知框架终止迭代。六、从“简单数据流图”看 input_node 的完整用法input_node通常用来替换显式循环的try_put写法。以文档配套的Simple Data Flow Graph即前文配图所示结构源节点生成 1~10平方/立方两条并行分支最终求和为例三种实现逐步演进实现一显式循环 try_putint sum 0; graph g; function_node int, int squarer( g, unlimited, [](const int v) { return v*v; } ); function_node int, int cuber( g, unlimited, [](const int v) { return v*v*v; } ); function_node int, int summer( g, 1, - int { return sum v; } ); make_edge( squarer, summer ); make_edge( cuber, summer ); for ( int i 1; i 10; i ) { squarer.try_put(i); cuber.try_put(i); } g.wait_for_all(); cout Sum is sum \n;实现二broadcast_node 合并 try_putbroadcast_nodeint b(g); make_edge( b, squarer ); make_edge( b, cuber ); for ( int i 1; i 10; i ) { b.try_put(i); } g.wait_for_all();实现三input_node 取代循环input_node int src( g, src_body(10) ); make_edge( src, squarer ); make_edge( src, cuber ); src.activate(); g.wait_for_all();三者均输出Sum is 42351~10 的平方和与立方和之和。其中squarer、cuber无副作用可设为unlimited并发summer通过引用更新共享变量sum并发不安全故并发度限制为 1构造函数第二参数。完整代码见 Data_Flow_Graph.rst。关于try_put与make_edge的关系补充未建边时对function_node调用try_put仍可投递消息flow_graph.h而make_edge(p, s)的本质是把后继节点注册进前驱的 successor 缓存flow_graph.h这正是 3.1 节所述“建边可能触发消息生产”的机制来源。七、为什么值得用 input_node对下游的背压响应在如此简单的例子中input_node相比显式循环并没有太多优势。文档强调它的真正价值在更复杂的图中because aninput_nodeis able to react to the behavior of downstream nodes, it can limit memory use in more complex graphs.其原理可以从apply_body_bypassflow_graph.h的调用链理解graph_task* apply_body_bypass( ) { output_type v; if ( !try_reserve_apply_body(v) ) return nullptr; graph_task *last_task my_successors.try_put_task(v); if ( last_task ) try_consume(); else try_release(); return last_task; }要点input_node每次调用 body 只“预取”一条消息try_reserve_apply_body并通过try_put_task尝试投递给后继若后继当前无法接收例如下游是受限并发节点且已满载try_put_task返回空指针此时try_release()释放该条消息节点暂停生产等待下游就绪后再由spawn_put重新调度因此input_node能根据下游的接收能力自动节流背压而不是像显式循环那样无脑地把数据灌进图内。在需要限制内存占用的复杂流图中这可以避免消息在中间缓冲节点堆积。八、实践要点速查针对use_input_node.rst的核心内容总结出以下可直接套用的实践规则场景推荐做法理由一般情况最安全非激活构造 → 全部建边 →activate()→g.wait_for_all()保证所有后继在建边完成前收不到任何消息杜绝漏发DAG input_node单后继希望构建与执行重叠按逆拓扑序建边深层边先建input_node构造后即可activate()每个节点在建边完成时都有完整后继消息不会因后继缺失而丢弃多个后继的input_node不要在全部建边前激活先挂接的后继会抢先收到消息后挂接的后继会漏收数据生产终止body 内数据耗尽时调用fc.stop()并返回默认值框架检查is_pipeline_stopped后停止迭代该返回值不会下发输出类型选择满足std::copyable如int、指针、可拷贝结构体input_nodeOutput有__TBB_requires(std::copyableOutput)约束进一步阅读本文主题文档use_input_node.rst配套完整示例Data_Flow_Graph.rst节点类型总览Predefined_Node_Types.rst建边与消息投递Edges.rst流图消息传递协议Flow_Graph_Message_Passing_Protocol.rst核心源码flow_graph.hinput_node类位于第 647–873 行、flow_control 定义【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考