mold 项目内嵌 TBB Flow Graph 的 sender 模板类:消息发送方接口与弃用迁移指南
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
导读
sender<T>是 oneTBB Flow Graph 中定义"消息发送方"行为的抽象基类,也是理解整个 flow graph 数据流方向的起点。本文以 mold 仓库中随附的 oneTBB 官方规范文档 sender_cls.rst 为主体,完整梳理该模板类的语法、头文件、成员语义与设计要点,并结合仓库内 flow_graph.h 的实际实现说明其底层行为,同时给出register_successor已弃用、应改用make_edge/remove_edge的迁移指南。读完本文,你将能够准确理解 sender 接口的每个虚函数职责、如何在自定义节点中继承并实现 sender,以及如何正确构建和拆除图中的边。
说明:本文讨论的
sender类来自 mold 仓库内嵌的 oneTBB(Threading Building Blocks)第三方库,该库随 mold 一同分发,路径位于 third-party/tbb。虽然 mold 是高性能链接器,其内嵌 TBB 主要用于自身并行化,但 TBB Flow Graph 是完整独立的数据流编程框架,本文仅聚焦 flow graph 的 sender 接口这一主题。
概述:sender 在 Flow Graph 中的角色
在 oneTBB Flow Graph 模型中,图由**节点(node)和边(edge)**组成,消息沿着边从生产者流向消费者。sender<T>就是描述"能够向外发送类型为T的消息"的节点的抽象基类。与之对应的是receiver<T>,描述"能够接收类型为T的消息"的节点。
根据规范文档的定义:
An abstract base class for nodes that act as message senders.
即:sender 是"充当消息发送方"的节点的抽象基类。它同时提供了若干函数的默认实现(default implementations),因此派生节点只需实现必要的最小接口即可。
从 flow_graph.h 的源码注释可以看到,oneTBB 将 sender 描述为 "Pure virtual template class that defines a sender of messages of type T"——一个定义"类型 T 的消息发送方"的纯虚模板类。这里的T即节点输出消息的类型。
需要特别注意的是,规范文档在开头就给出了caution(警告):
This feature is deprecated and will be reworked or removed in the future.
也就是说,sender类本身已被标记为弃用(deprecated),未来可能被重构或移除。这一点在使用时应心中有数。
语法与头文件
模板声明
template< typename T > class sender;sender是一个以消息类型T为模板参数的类模板,使用位置如sender<int>、sender<my_message>等。
头文件
#include "oneapi/tbb/flow_graph.h"在 mold 仓库中,该头文件的真实路径为 third-party/tbb/include/oneapi/tbb/flow_graph.h,同目录下还提供了传统路径的兼容头文件 third-party/tbb/include/tbb/flow_graph.h。注意,实际实现位于oneapi::tbb::detail::d2命名空间,并通过头文件底部的命名空间别名机制暴露到oneapi::tbb::flow,这与文档给出的使用命名空间一致:
namespace oneapi { namespace tbb { namespace flow { // 用户实际使用的 sender 位于此命名空间 } } }完整成员定义
规范文档给出了sender的完整成员列表(见 sender_cls.rst):
template< typename T > class sender { public: typedef T output_type; typedef receiver<output_type> successor_type; virtual ~sender(); virtual bool register_successor( successor_type &r ) = 0; virtual bool remove_successor( successor_type &r ) = 0; virtual bool try_get( output_type &v ) { return false; } virtual bool try_reserve( output_type &v ) { return false; } virtual bool try_release( ) { return false; } virtual bool try_consume( ) { return false; } };与仓库内 flow_graph.h 的实现在成员集合上完全一致(仓库版本将output_type与successor_type放在protected区,并将两个纯虚函数设为protected以配合register_successor/remove_successor的友元自由函数,这是实现细节上的差异,接口语义不变)。
其中:
| 类型别名 | 含义 |
|---|---|
output_type | 本 sender 输出的消息类型,即模板参数T |
successor_type | 后继节点类型,即receiver<output_type>,表示"能够接收本节点输出消息的接收方" |
成员函数语义表
规范文档用一张表逐项说明了每个成员的语义(见 sender_cls.rst),整理如下:
| 成员 | 说明 | 返回值 |
|---|---|---|
~sender() | 虚析构函数 | — |
bool register_successor( successor_type &r ) = 0 | 纯虚方法,定义"向 sender 的后继集合中添加一个后继节点"的接口 | 添加成功返回true,否则返回false |
bool remove_successor( successor_type &r ) = 0 | 纯虚方法,定义"从 sender 的后继集合中移除一个后继节点"的接口 | 移除成功返回true,否则返回false |
bool try_get( output_type &v ) | 从 sender 请求一个消息项 | 默认实现返回false |
bool try_reserve( output_type &v ) | 在 sender 处保留(reserve)一个消息项 | 默认实现返回false |
bool try_release( ) | 释放 sender 上持有的保留(reservation) | 默认实现返回false |
bool try_consume( ) | 消耗 sender 上持有的保留(reservation) | 默认实现返回false |
两类成员的不同性质
理解这张表的关键在于区分两类成员:
纯虚函数(必须实现):
register_successor与remove_successor是= 0的纯虚函数,任何直接继承sender<T>的自定义节点都必须提供实现,否则无法实例化。它们负责维护"后继节点集合"这一核心数据结构,是消息转发路径的根基。虚函数(可选覆写):
try_get、try_reserve、try_release、try_consume均有默认实现且默认返回false。这意味着:默认情况下,sender 既不支持被"拉取"(pull),也不支持"保留-消耗"(reserve/consume)协议;只有覆写它们、返回true并真正实现相应行为的节点,才支持这些高级交互方式。
try_get / try_reserve / try_release / try_consume 的交互协议
规范文档分别描述了这四个函数的功能,它们共同构成一个完整的消息保留协议:
try_get(v):请求一个消息项,成功时把消息写入v并返回true。try_reserve(v):请求"保留"一个消息项,成功时写入v并返回true。保留意味着该项暂时锁定,不能被其他消费者取走。try_release():释放此前通过try_reserve取得的保留,消息项重新变为可获取状态。try_consume():将此前保留的消息项正式消耗掉(移除),完成消费。
典型流程为:try_reserve(v)成功后,消费者可选择try_consume()(真正取走)或try_release()(放弃)。在 flow_graph.h 中,limiter_node、buffer_node、queue_node、overwrite_node等缓冲类节点都覆写了这套协议,以支持基于保留的消息调度(例如reserving_arc形式的边会调用try_reserve决定是否可建立输送)。
源码实现佐证:从 sender 到边
核心实现位置
mold 仓库内嵌 oneTBB 中 sender 的核心实现位于 flow_graph.h,关键点如下:
- 类注释明确其为 "Pure virtual template class";
- 每个虚函数都带有简要注释,如
//! Request an item from the sender、//! Reserves an item in the sender、//! Releases the reserved item、//! Consumes the reserved item; - 纯虚函数
register_successor/remove_successor被声明为protected,并分别声明了两个模板友元函数register_successor(sender<C>&, receiver<C>&)与remove_successor(sender<C>&, receiver<C>&)(见 flow_graph.h),自由函数内部只是简单转发到对应的成员函数。
典型实现:broadcast_node / buffer_node
仓库中许多具体节点都继承自sender<T>(同时继承receiver<T>),例如:
- broadcast_node:注释为 "Forwards messages of type T to all successors",把收到的每个消息转发给所有后继,内部用
broadcast_cache<input_type>维护后继集合; - buffer_node:同时继承
reservable_item_buffer、receiver<T>与sender<T>,是一个可缓冲、支持保留协议的消息管道节点; queue_node、overwrite_node、source_node、multifunction_node、composite_node等同样通过继承 sender/receiver 获得收发能力(composite_node的output_ports_type定义为std::tuple< sender<OutputTypes>&... >,见 flow_graph.h)。
可以推断:一个节点"既能收又能发"时,通常同时继承receiver<T>与sender<T>;而纯数据源节点(如source_node)则主要作为 sender 存在。
边的构建与拆除:make_edge / remove_edge
规范文档在 Description 一节给出了第二个caution(重要警告):
The use of
register_successorto build graphs has been deprecated forflow::graph. Graphs should be constructed withmake_edgeandremove_edge. Replacen1.register_successor(n2)withmake_edge(n1,n2).
即:使用register_successor构建图已被弃用,应改用make_edge和remove_edge构建与拆除边;将n1.register_successor(n2)替换为make_edge(n1, n2)。
仓库实现印证了这一点:flow_graph.h 中make_edge经由internal_make_edge调用register_successor(p, s)再记录 profiling 事件(fgt_make_edge);remove_edge 则调用remove_successor(p, s)并记录fgt_remove_edge。也就是说,make_edge/remove_edge正是对弃用接口的安全封装,内部仍复用后继集合的增删逻辑,但对外提供了更干净、更安全的 API,并且支持多种重载:
make_edge(sender<T>&, receiver<T>&):单输入单输出节点之间建边;- 当节点具有
output_ports_type/input_ports_type(多端口节点,如multifunction_node、split_node、indexer_node)时,自动选取端口 0 建边(见 flow_graph.h),remove_edge提供完全对称的重载集合(见 flow_graph.h)。
迁移指南:从 register_successor 到 make_edge
根据规范文档的明确指示,把旧式建边代码迁移到新式 API 非常简单:
迁移前(已弃用):
// 旧式:直接操作后继集合 n1.register_successor(n2); n1.remove_successor(n2);迁移后(推荐):
// 新式:通过自由函数建边/拆边 make_edge(n1, n2); remove_edge(n1, n2);注意事项:
make_edge与remove_edge要求两端的消息类型匹配(sender<T>连到receiver<T>),类型不匹配会在编译期报错,这也是比直接调用register_successor更安全的原因之一;- 对多端口节点,
make_edge/remove_edge会自动处理端口 0 的接线,无需手动从output_ports()/input_ports()中取端口; - 虽然
register_successor/remove_successor仍是纯虚函数、任何 sender 派生类都必须实现,但用户代码不应直接调用它们来建图;make_edge内部会完成转发与 profiling 记录。
自定义 sender 派生类的实践要点
若需要在 mold 仓库内嵌的 TBB 中编写自定义节点并使其具备发送能力,可以从 sender 的接口设计中得到以下实践要点:
- 必须实现
register_successor(successor_type&)与remove_successor(successor_type&),用一个容器(如std::vector<successor_type*>或 TBB 提供的broadcast_cache<T>、round_robin_cache<T>等后继缓存类)维护后继集合; - 默认即可编译运行:不覆写
try_get/try_reserve/try_release/try_consume时它们返回false,节点表现为"只推不拉、不支持保留"; - 按需覆写拉取与保留协议:需要支持下游拉取(pull-based)或保留式调度(如连接
limiter_node)时,再覆写try_get及 reserve/release/consume 三件套,并保证状态机一致(reserve 后必须 consume 或 release 二选一); - 建图一律走
make_edge/remove_edge,不要直接调用register_successor; - 注意弃用风险:
sender类整体已被标记为 will be reworked or removed,若编写面向未来的新代码,建议尽量基于高层节点(function_node、buffer_node、multifunction_node、composite_node等)组合图结构,而非直接继承sender。
验证与测试
mold 仓库内嵌的 oneTBB 自带完整测试套件,位于 third-party/tbb/test(含test_flow_graph*.cpp等用例),以及文档目录 third-party/tbb/doc/main/specification(本文主体 sender_cls.rst 即位于其中的uncategorized/flow_graph分类下,同目录还收录了receiver_cls.rst等配套规范)。如需在本地验证 sender 相关行为,可参考 TBB 的构建方式(其 CMake 构建入口为 third-party/tbb/CMakeLists.txt)编译 flow graph 相关测试,或直接阅读头文件中的接口注释与节点实现来确认语义。
小结
sender<T>作为 oneTBB Flow Graph 的发送端抽象基类,通过"两个纯虚函数 + 四个带默认实现的虚函数"的极简接口,统一了所有消息发送方节点的行为契约:register_successor/remove_successor负责后继集合的维护,try_get/try_reserve/try_release/try_consume定义拉取与保留协议。在 mold 仓库内嵌的 flow_graph.h 中,broadcast_node、buffer_node、composite_node等节点都以它为基础构建,而make_edge/remove_edge则成为取代弃用接口、安全构建数据流图的推荐途径。理解 sender 的接口设计,是深入掌握 Flow Graph 消息传递机制、乃至自定义节点行为的关键一步。
【免费下载链接】moldmold: A Modern Linker 🦠项目地址: https://gitcode.com/GitHub_Trending/mo/mold
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考