news 2026/9/14 4:34:41

oneTBB Flow Graph 节点构造函数详解:用 follows / precedes 声明式构建依赖图

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oneTBB Flow Graph 节点构造函数详解:用 follows / precedes 声明式构建依赖图

oneTBB Flow Graph 节点构造函数详解:用 follows / precedes 声明式构建依赖图

【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold

本文聚焦于 vendored oneTBB 参考手册中的《Constructors for Flow Graph nodes》章节,系统讲解如何借助followsprecedes辅助函数、以声明式方式构造 oneTBB Flow Graph 中的各类节点,涵盖全部节点类型的构造函数签名、make_node_set配合用法、C++17 CTAD 支持,并结合仓库内 TBB 源码(flow_graph.h、_flow_graph_node_set_impl.h)剖析其底层实现原理与编译期约束。读完本文,你将能写出不再显式传graph&参数、边与节点一次性声明完成的流图构建代码,并理解该特性的适用前提与限制。

特性概述:为"表达图关系的辅助函数"新增构造函数

TBB_PREVIEW_FLOW_GRAPH_FEATURES预览特性为 oneTBB 流图新增了一组构造函数重载,用于构造一个跟随(follows)前置(precedes)一组节点的节点。在传统写法中,每个节点构造时都需要显式传入其所属的graph&,随后再用make_edge逐条连接边;而使用新构造函数后,节点间的拓扑关系可以在构造节点的那一刻一并声明,由库自动完成建边,代码如下更接近对数据流拓扑的"直接描述"。

需要特别说明的是,该特性由预览宏控制,默认处于关闭状态。要启用它,必须在包含头文件之前定义:

#define TBB_PREVIEW_FLOW_GRAPH_FEATURES 1 #include <oneapi/tbb/flow_graph.h>

从仓库源码 _config.h 可以看到,TBB_PREVIEW_FLOW_GRAPH_FEATURES在未定义时会回退到__TBB_CPF_BUILD的取值;而围绕它派生的开关还包括(见 _config.h):

  • __TBB_PREVIEW_FLOW_GRAPH_NODE_SET—— 本文所述node_set/follows/precedes/make_node_set机制的总开关;
  • __TBB_PREVIEW_MESSAGE_BASED_KEY_MATCHING__TBB_PREVIEW_FLOW_GRAPH_TRY_PUT_AND_WAIT__TBB_PREVIEW_FLOW_GRAPH_RESOURCE_LIMITING—— 同组预览特性。

在可能的情况下,这些构造函数支持C++17 的类模板参数推导(CTAD, Class Template Argument Deduction),即function_nodebroadcast_node等模板节点的类型参数可以由实参自动推导,进一步减少样板代码。

核心语义:follows 与 precedes

followsprecedes的完整语义定义在同目录的 follows_and_precedes_functions.rst 中,要点如下:

  • follows(...):正在构造的节点是指定节点集合的后继(successor),即消息从集合中的节点流入新节点;
  • precedes(...):正在构造的节点是指定节点集合的前驱(predecessor),即消息从新节点流向集合中的节点;
  • 这两个辅助函数只能用于节点构造期间,用来取代原本作为构造器第一个参数传入的graph&——新节点所属的图从传入的节点集合或节点序列中推导得出;
  • 如果传入的节点分属不同的图(graph),行为是未定义的(undefined),使用时应确保节点来自同一个graph实例。

从实现层面看(_flow_graph_node_set_impl.h),库内部用三个空标记类型表达集合的"顺序状态":

namespace order { struct undefined {}; struct following {}; struct preceding {}; }

followsprecedes各有两个重载:一个接收若干节点实参(变参序列),另一个接收make_node_set生成的节点集合。以follows为例(_flow_graph_node_set_impl.h):

template<typename FirstPredecessor, typename... Predecessors> node_set<order::following, FirstPredecessor, Predecessors...> follows(FirstPredecessor& first_predecessor, Predecessors&... predecessors) { static_assert((conjunction<has_output_type<FirstPredecessor>, has_output_type<Predecessors>...>::value), "Not all node's predecessors has output_type typedef"); static_assert((conjunction<is_sender<FirstPredecessor>, is_sender<Predecessors>...>::value), "Not all node's predecessors are senders"); return node_set<order::following, FirstPredecessor, Predecessors...>(first_predecessor, predecessors...); }

这里有两个值得注意的编译期约束:

  1. 作为follows参数的每个节点都必须具有output_type类型别名(否则会触发static_assert报错);
  2. 作为follows参数的每个节点都必须派生自sender<T>(即都是"发送方")。

对称地,precedes要求参数节点具备input_type类型别名、并且派生自receiver<T>(即都是"接收方"),对应实现见 _flow_graph_node_set_impl.h。这些约束在编译期就把"把错误类型的节点当作前驱/后继"的问题拦截掉了。

用 make_node_set 组装节点集合

make_node_set函数模板用于创建一个"节点集合"对象,该对象可作为make_edgesfollowsprecedes的参数(make_node_set_function.rst):

#include <oneapi/tbb/flow_graph.h> template <typename Node, typename... Nodes> /*unspecified*/ make_node_set( Node& node, Nodes&... nodes );

两种传参方式完全等价(示例来自 follows_and_precedes_functions.rst):

// 方式一:先组装集合,再传给 precedes auto handlers = make_node_set(n1, n2, n3); broadcast_node<int> input(precedes(handlers)); // 方式二:直接把节点序列传给 precedes broadcast_node<int> input(precedes(n1, n2, n3));

其实现(_flow_graph_node_set_impl.h)只是把传入的节点引用包装进一个以order::undefined为顺序标记的node_set,后续交给follows/precedes时再转换为带方向的集合。

支持新构造函数的节点类型总览

原文档 constructors_for_nodes.rst 给出的完整构造函数语法如下,涵盖 oneTBB 流图的大部分常用节点:

// continue_node continue_node(follows(...), Body body, Policy = Policy()); continue_node(precedes(...), Body body, Policy = Policy()); continue_node(follows(...), int number_of_predecessors, Body body, Policy = Policy()); continue_node(precedes(...), int number_of_predecessors, Body body, Policy = Policy()); // function_node function_node(follows(...), std::size_t concurrency, Policy = Policy()); function_node(precedes(...), std::size_t concurrency, Policy = Policy()); // input_node input_node(precedes(...), body); // multifunction_node multifunction_node(follows(...), std::size_t concurrency, Body body); multifunction_node(precedes(...), std::size_t concurrency, Body body); // async_node async_node(follows(...), std::size_t concurrency, Body body); async_node(precedes(...), std::size_t concurrency, Body body); // overwrite_node explicit overwrite_node(follows(...)); explicit overwrite_node(precedes(...)); // write_once_node explicit write_once_node(follows(...)); explicit write_once_node(precedes(...)); // buffer_node explicit buffer_node(follows(...)); explicit buffer_node(precedes(...)); // queue_node explicit queue_node(follows(...)); explicit queue_node(precedes(...)); // priority_queue_node explicit priority_queue_node(follows(...), const Compare& comp = Compare()); explicit priority_queue_node(precedes(...), const Compare& compare = Compare()); // sequencer_node sequencer_node(follows(...), const Sequencer& s); sequencer_node(precedes(...), const Sequencer& s); // limiter_node limiter_node(follows(...), std::size_t threshold); limiter_node(precedes(...), std::size_t threshold); // broadcast_node explicit broadcast_node(follows(...)); explicit broadcast_node(precedes(...)); // join_node explicit join_node(follows(...), Policy = Policy()); explicit join_node(precedes(...), Policy = Policy()); // split_node explicit split_node(follows(...)); explicit split_node(precedes(...)); // indexer_node indexer_node(follows(...)); indexer_node(precedes(...));

使用要点:

  • 上述follows(...)/precedes(...)占位符既可展开为follows(n1, n2, ...)的节点序列形式,也可展开为follows(make_node_set(n1, n2, ...))的集合形式;
  • 除构造参数本身外,各节点原有的业务参数(Body bodyconcurrencythresholdCompareSequencerPolicy等)位置与语义不变,因此新构造函数是对旧构造函数的无损替代
  • continue_node额外保留了int number_of_predecessors参数版本,用于声明需要等待的前驱数量(与普通continue_node(g, count, body, policy)一致);
  • 标记为explicit的节点(如overwrite_nodebuffer_nodebroadcast_node等)禁止隐式转换,构造时须显式写出类型。

与传统写法的对比

continue_node为例。传统写法需要显式传入图对象,并逐条调用make_edge

graph g; continue_node<continue_msg> A(g, [](continue_msg) { /* ... */ }); continue_node<continue_msg> B(g, [](continue_msg) { /* ... */ }); make_edge(A, B); // 手动建边

新写法把"属于哪个图"与"与谁相连"统一在构造函数中声明:

graph g; continue_node<continue_msg> A(g, [](continue_msg) { /* ... */ }); // B 跟随 A:B 的图自动取自 A,且 A->B 的边自动建立 continue_node<continue_msg> B(follows(A), [](continue_msg) { /* ... */ });

若使用 CTAD(C++17),continue_node的类型参数也可省略:

graph g; continue_node A(g, [](continue_msg) { return 0; }); // C++17 CTAD continue_node B(follows(A), [](continue_msg) { return 0; });

源码级原理:图获取、多端口选择与自动建边

新构造函数能够"免传graph&",关键在于节点集合可以从成员节点推导出所属图。其实现链条清晰地位于 _flow_graph_node_set_impl.h 中。

1. 从节点推导 graph

node_set内部用std::tuple<Nodes&...>持有各节点引用,并通过graph_reference()暴露所属图(_flow_graph_node_set_impl.h):

template<typename Order, typename... Nodes> struct node_set { typedef Order order_type; std::tuple<Nodes&...> nodes; node_set(Nodes&... ns) : nodes(ns...) {} template <typename... Nodes2> node_set(const node_set<order::undefined, Nodes2...>& set) : nodes(set.nodes) {} graph& graph_reference() const { return get_graph_helper::get(std::get<0>(nodes)); } };

graph_reference()第一个节点作为图来源,通过get_graph_helper实现(_flow_graph_node_set_impl.h):若节点派生自graph_node基类,则直接取其内部my_graph成员;否则回退到对象自身的graph_reference()方法。这也从实现上解释了文档中"节点必须属于同一图,否则行为未定义"的约束——图信息只从首个节点取出,跨图混用必然产生错误语义。

在节点构造函数侧,如 flow_graph.h 中continue_node的新构造函数所示,它先通过nodes.graph_reference()解析出图,再委托给传统构造函数完成初始化,随后按集合方向自动建边:

continue_node( const node_set<Args...>& nodes, Body body, Policy p = Policy(), node_priority_t a_priority = no_priority ) : continue_node(nodes.graph_reference(), body, p, a_priority ) { ... }

input_node同样只接受node_set<order::preceding, Successors...>形式(见 flow_graph.h),与文档语法input_node(precedes(...), body)一一对应。

2. 多端口节点的端口选择

join_nodesplit_nodeindexer_node等多端口节点拥有多个输入/输出端口,直接对节点建边是不够的。库为此提供了端口选择器(_flow_graph_node_set_impl.h):

  • successor_selector<I>::get(node):若节点具有input_ports_type(多端口),返回第Iinput_port<I>(node);否则返回节点自身;
  • predecessor_selector<I>::get(node):若节点具有output_ports_type(多端口),返回第Ioutput_port<I>(node);若节点是async_node则返回节点自身;否则返回节点自身。

这意味着precedes(n1, n2)中的n1n2若为多端口节点,将按参数顺序分别连接到新节点的第 0、1 号输出端口——顺序敏感,使用时应保证参数顺序与端口编号对应。

3. 自动建边

make_edges_helper通过模板递归从第N-1号端口一路回连到第 0 号端口(_flow_graph_node_set_impl.h):

template<typename PredecessorsTuple, typename NodeType> static void connect_predecessors(PredecessorsTuple& predecessors, NodeType& node) { make_edge(std::get<I>(predecessors), successor_selector<I>::get(node)); make_edges_helper<I - 1>::connect_predecessors(predecessors, node); }

make_edges_in_order则根据集合的order标记决定建边方向(_flow_graph_node_set_impl.h):order::following集合作为前驱(make_edges(ns, node)),order::preceding集合作为后继(make_edges(node, ns))。整个"方向标记 + 端口选择 + 递归建边"的机制,正是follows/precedes声明式语法能在构造时自动完成连线的原因。

实战示例:组合使用各节点类型

下面构造一个具有代表性的流图:input_node产出消息,经broadcast_node扇出到两个分支,其中一个分支经过function_node变换后与另一分支在join_node汇合。全程不显式传graph&、不调用make_edge

#define TBB_PREVIEW_FLOW_GRAPH_FEATURES 1 #include <oneapi/tbb/flow_graph.h> #include <cstdio> using namespace oneapi::tbb::flow; int main() { graph g; // 数据源:precedes 声明它扇出的所有后继 input_node<int> source(precedes(bcast), & -> int { static int i = 0; if (i < 3) return i++; fc.stop(); return 0; }); // 扇出节点:follows 声明前驱(source),precedes 声明两个后继 broadcast_node<int> bcast(follows(source), precedes(adder, joiner)); // 变换节点:跟随 bcast function_node<int, int> adder(follows(bcast), unlimited, [](int v) { return v * 10; }); // 汇合节点:跟随 bcast 与 adder 两个前驱 join_node<std::tuple<int, int>> joiner(follows(bcast, adder)); function_node<std::tuple<int, int>> sink(follows(joiner), unlimited, [](std::tuple<int, int> t) { std::printf("(%d, %d)\n", std::get<0>(t), std::get<1>(t)); }); source.activate(); g.wait_for_all(); return 0; }

注意:bcast同时出现在followsprecedes中,说明一个节点可以同时是某个集合的后继、另一个集合的前驱;所有节点均属于同一个graph g,满足"同图"前提。若需要把前驱/后继数量动态化或复用同一组节点多次,可先用make_node_set组装集合再传入。

限制与注意事项

  • 预览特性:必须定义TBB_PREVIEW_FLOW_GRAPH_FEATURES 1,且该宏同时控制同组的多个预览开关;预览 API 在后续版本中可能调整。
  • 仅限构造期follows/precedes只能在节点构造时使用,不能在运行时修改已有节点的连接关系。
  • 同图约束:传入follows/precedes的所有节点必须属于同一graph,否则行为未定义。
  • 编译期校验follows要求参数具备output_type且为senderprecedes要求参数具备input_type且为receiver,违反者直接触发static_assert
  • 端口顺序敏感:对多端口节点(join_nodesplit_nodeindexer_node)建边时按参数顺序映射端口;async_node在前驱侧有专门处理。
  • C++ 标准:CTAD 特性需要 C++17 或更高版本;不使用 CTAD 时仍需显式写出模板参数。

延伸阅读

  • follows 与 precedes 函数模板:语义定义与输入参数等价形式;
  • make_node_set 函数模板:节点集合的组装方式与make_edges配合用法;
  • flow_graph.h:各节点类的完整构造器与行为说明;
  • _flow_graph_node_set_impl.h:node_set、端口选择器与自动建边的完整实现。

需要说明的是,本仓库(mold 链接器)以 vendored 方式在 third-party/tbb 中携带了完整 oneTBB 源码及其文档,mold 自身在 src/icf.cc、src/gdb-index.cc、src/cmdline.cc 等处主要使用 TBB 的并行算法与并发容器来加速链接过程;而本文介绍的 Flow Graph 节点构造函数属于 TBB 流图模块的独立特性,可在任何基于 oneTBB 的 C++ 项目中按上述方式直接使用。

【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 4:34:37

Klipper 3D打印固件实战指南:部署校准与质量调优全流程

Klipper 3D打印固件实战指南&#xff1a;部署校准与质量调优全流程 【免费下载链接】klipper Klipper is a 3d-printer firmware 项目地址: https://gitcode.com/GitHub_Trending/kl/klipper 拐角处的振铃、外壁上的重影&#xff0c;这类打印缺陷靠拧紧皮带解决不了。Kl…

作者头像 李华
网站建设 2026/9/14 4:27:40

ELK+Filebeat+Kafka日志分析平台在Rocky Linux 9.4上的搭建实践

先聊点实际的。做运维干了这么多年&#xff0c;我最怕听到的一句话就是“日志丢哪儿去了&#xff1f;”线上应用一抖动&#xff0c;开发、DBA、业务方全来找你。早期我也靠ssh到每台机器上敲tail -f&#xff0c;机器少点还能硬扛&#xff0c;等规模上来之后&#xff0c;几十台服…

作者头像 李华
网站建设 2026/9/14 4:23:54

数学建模也能Agent化:多智能体协作全流程实战

我去年年底开始认真琢磨一件事&#xff1a;数学建模这个场景&#xff0c;能不能被Agent化。先说背景。我本职做算法工程&#xff0c;业余会帮学生辅导数学建模竞赛&#xff0c;也接一些企业里的小型建模咨询。前几年大家普遍的做法&#xff0c;是把ChatGPT当高级搜索引擎用——…

作者头像 李华