oneTBB Flow Graph 节点构造函数详解:用 follows / precedes 声明式构建依赖图
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
本文聚焦于 vendored oneTBB 参考手册中的《Constructors for Flow Graph nodes》章节,系统讲解如何借助follows与precedes辅助函数、以声明式方式构造 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_node、broadcast_node等模板节点的类型参数可以由实参自动推导,进一步减少样板代码。
核心语义:follows 与 precedes
follows与precedes的完整语义定义在同目录的 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 {}; }follows与precedes各有两个重载:一个接收若干节点实参(变参序列),另一个接收由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...); }这里有两个值得注意的编译期约束:
- 作为
follows参数的每个节点都必须具有output_type类型别名(否则会触发static_assert报错); - 作为
follows参数的每个节点都必须派生自sender<T>(即都是"发送方")。
对称地,precedes要求参数节点具备input_type类型别名、并且派生自receiver<T>(即都是"接收方"),对应实现见 _flow_graph_node_set_impl.h。这些约束在编译期就把"把错误类型的节点当作前驱/后继"的问题拦截掉了。
用 make_node_set 组装节点集合
make_node_set函数模板用于创建一个"节点集合"对象,该对象可作为make_edges、follows、precedes的参数(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 body、concurrency、threshold、Compare、Sequencer、Policy等)位置与语义不变,因此新构造函数是对旧构造函数的无损替代; continue_node额外保留了int number_of_predecessors参数版本,用于声明需要等待的前驱数量(与普通continue_node(g, count, body, policy)一致);- 标记为
explicit的节点(如overwrite_node、buffer_node、broadcast_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_node、split_node、indexer_node等多端口节点拥有多个输入/输出端口,直接对节点建边是不够的。库为此提供了端口选择器(_flow_graph_node_set_impl.h):
successor_selector<I>::get(node):若节点具有input_ports_type(多端口),返回第I个input_port<I>(node);否则返回节点自身;predecessor_selector<I>::get(node):若节点具有output_ports_type(多端口),返回第I个output_port<I>(node);若节点是async_node则返回节点自身;否则返回节点自身。
这意味着precedes(n1, n2)中的n1、n2若为多端口节点,将按参数顺序分别连接到新节点的第 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同时出现在follows与precedes中,说明一个节点可以同时是某个集合的后继、另一个集合的前驱;所有节点均属于同一个graph g,满足"同图"前提。若需要把前驱/后继数量动态化或复用同一组节点多次,可先用make_node_set组装集合再传入。
限制与注意事项
- 预览特性:必须定义
TBB_PREVIEW_FLOW_GRAPH_FEATURES 1,且该宏同时控制同组的多个预览开关;预览 API 在后续版本中可能调整。 - 仅限构造期:
follows/precedes只能在节点构造时使用,不能在运行时修改已有节点的连接关系。 - 同图约束:传入
follows/precedes的所有节点必须属于同一graph,否则行为未定义。 - 编译期校验:
follows要求参数具备output_type且为sender,precedes要求参数具备input_type且为receiver,违反者直接触发static_assert。 - 端口顺序敏感:对多端口节点(
join_node、split_node、indexer_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),仅供参考