十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

mold 内嵌 OneTBB flow_graph 的 queue_node:FIFO 缓冲节点的规范解读与源码剖析

mold 内嵌 OneTBB flow_graph 的 queue_node:FIFO 缓冲节点的规范解读与源码剖析 mold 内嵌 OneTBB flow_graph 的 queue_node:FIFO 缓冲节点的规范解读与源码剖析【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本文基于 OneTBB(随 mold 以第三方库形式引入)flow_graph 规范文档,系统讲解queue_node这一 FIFO(先进先出)消息转发缓冲节点的设计语义、成员函数契约与转发/缓冲策略,并对照 OneTBB 头文件源码剖析其“从队头弹出、单推转发、可保留缓冲”的实现细节,帮助你在并行任务图中准确选用这一节点,并理解它与buffer_node、priority_queue_node、sequencer_node等兄弟节点的本质区别。1. queue_node 是什么:flow_graph 中的 FIFO 缓冲节点在 flow_graph 的节点类型体系中,queue_node的定位非常明确——规范原文给出的定义是:A node that forwards messages in a first-in first-out (FIFO) order. (以先进先出顺序转发消息的节点)其规范接口声明如下(出自 queue_node 规范):// Defined in header oneapi/tbb/flow_graph.h namespace oneapi { namespace tbb { namespace flow { template typename T class queue_node : public graph_node, public receiverT, public senderT { public: explicit queue_node( graph g ); queue_node( const queue_node src ); ~queue_node(); bool try_put( const T v ); bool try_get( T v ); }; } // namespace flow } // namespace tbb } // namespace oneapi从这份声明可以直接读出三件事:三重身份:queue_node同时是graph_node(图的成员)、receiverT(可以接收消息)、senderT(可以向后继发送消息)。这意味着它既可以作为“生产者”被上游make_edge喂消息,也可以作为“消费者”被下游主动拉取。模板类型约束:规范明确要求T满足 ISO C 标准中CopyConstructible与CopyAssignable的要求——消息在图中流转时会被复制进出缓冲区。FIFO 顺序 单推语义:规范正文指出,queue_node以 FIFO 顺序把消息转发给其后继集合中的单个后继(succeeding successor);若所有后继都拒绝接收,消息会保留在节点内部等待重发。1.1 缓冲策略与转发策略:buffering single-pushflow_graph 的节点都有两个正交的策略维度,queue_node在这两个维度上的取值定义见 转发与缓冲策略汇总:策略维度可选值queue_node 的取值含义转发策略 (Forwarding)broadcast-push / single-pushsingle-push消息被某一个后继接受后,就不再尝试推送给其余后继;若当前后继拒绝,则尝试后继集合中的下一个,直到有后继接受或全部尝试完毕缓冲策略 (Buffering)buffering / discardingbuffering没有任何后继接受消息时,消息被存储起来,等待后续节点处理时使用(因此try_get()可用)规范中的策略汇总表把queue_node明确归入Buffering Nodes(缓冲节点)一行:try_get()? yes,Forwarding single-push。与之同组的还有buffer_node、priority_queue_node、sequencer_node(均为 single-push),以及overwrite_node、write_once_node(broadcast-push)。这解释了为什么用户指南将queue_node与buffer_node等并列为“预定义缓冲节点”(见 预定义节点类型)。2. 成员函数语义:完整继承规范契约规范对每个成员函数给出了精确的行为契约,这部分是使用时最需要逐字理解的。2.1 构造函数explicit queue_node( graph g ); queue_node( const queue_node src );显式构造函数:构造一个属于图g的空queue_node。拷贝构造函数:构造一个属于与src同一个图g的空queue_node;src的中间状态(包括它到前驱/后继的边)都不会被拷贝。也就是说,拷贝出来的节点是一个“干净”的容器,你需要重新用make_edge建立连接。2.2try_put:入队并尝试转发最早的消息bool try_put( const T v );效果:把v加入该节点管理的元素集合(入队),然后尝试把最早加入的元素(即队头)转发给某个后继。返回值:恒为true。由于是 buffering 节点,入队永远会成功,不会像 rejecting 型function_node那样因“忙”而拒绝。注意这里体现的 FIFO 语义:你try_put进来的新消息排到队尾,真正被优先推送出去的是队列中最旧的那一条。2.3try_get:主动从节点拉取消息bool try_get( T v );返回true:当节点中有元素可取,并把该元素赋给v(FIFO 语义下取的是队头)。返回false:节点当前没有任何元素,或者节点处于**保留(reserved)**状态。保留状态来自 flow_graph 的预留协议:一个 sender 可以先try_reserve再try_consume/try_release,在保留期间其他try_get会失败,从而避免竞争消费。规范特意强调“node is reserved”这一失败原因,是在提示你:在多消费者场景下,try_get返回 false 不一定代表队列已空。2.4 使用场景规范 Example 一节的原文非常简短但信息量大:Usage scenario is similar to buffer_node except that messages are passed in first-in first-out (FIFO) order.即:一切buffer_node适用的场景(削峰、解耦生产/消费速率、为后继提供可拉取的缓冲)都适用于queue_node,唯一区别是消费顺序——buffer_node是 LIFO(栈语义,try_get/转发取的是最近入队的消息),queue_node是 FIFO(队列语义,严格保序)。在任务流水线里,如果你要求“谁先到就谁先被处理”,就必须选queue_node而不是buffer_node;如果只关心吞吐、不关心顺序,两者皆可。3. 源码级剖析:OneTBB 头文件中的 queue_node规范描述的是抽象契约,而 mold 仓库内 vendored 的 OneTBB 实现位于 flow_graph.h。对照源码,可以看清 FIFO 语义与 single-push 策略究竟是如何落地的。3.1 类继承结构:queue_node 是 buffer_node 的“队头版”从源码结构看,queue_node并不像规范声明那样直接继承三个基类,而是继承自buffer_nodeT(buffer_node本身已实现 graph_node/receiver/sender 的全部能力):// flow_graph.h L1695-L1697 //! Forwards messages in FIFO order template typename T class queue_node : public buffer_nodeT {见 queue_node 类定义。它相对buffer_node的全部差异,就是把“取哪个端元素”的操作从队尾换到了队头,这正是 LIFO 与 FIFO 的分水岭:buffer_node的默认弹出实现(对应其 LIFO 语义)使用pop_back:// flow_graph.h L1491-L1505,buffer_node 的 internal_pop virtual void internal_pop(buffer_operation *op) { ... bool pop_result op-metainfo ? this-pop_back(*(op-elem), *(op-metainfo)) : this-pop_back(*(op-elem)); ... }queue_node则重写了internal_pop,改为pop_front,并且失败条件包含“节点被保留”这一项,与规范对try_get的描述严格对应:// flow_graph.h L1728-L1743,queue_node 的 internal_pop void internal_pop(queue_operation *op) override { if ( this-my_reserved || !this-my_item_valid(this-my_head)){ op-status.store(FAILED, std::memory_order_release); } else { this-pop_front(*(op-elem)); op-status.store(SUCCEEDED, std::memory_order_release); } }注意这里的my_item_valid(this-my_head):buffer_node 判断有效性时看my_tail - 1(最近入队端),而queue_node的is_item_valid()检查的是my_head(最早入队端),见 is_item_valid。3.2 转发路径:总是推队头,成功后销毁队头queue_node重写了转发所用的“推送一个元素并处理结果”函数,推送的永远是front():// flow_graph.h L1711-L1721 void try_put_and_add_task(graph_task* last_task) { graph_task* new_task this-my_successors.try_put_task(this-front() ...); if (new_task) { graph graph_ref this-graph_reference(); last_task combine_tasks(graph_ref, last_task, new_task); this-destroy_front(); } }见 try_put_and_add_task。与之对照,buffer_node基类中同名函数推的是back()并在成功后destroy_back()。这个front/back的一字之差,就是两种缓冲节点顺序语义的全部实现差异。3.3 single-push 策略的实现:转发循环真正体现 single-push buffering 策略的是buffer_node提供的共享转发实现internal_forward_task_impl,queue_node通过 CRTP 风格的derived模板参数复用它:// flow_graph.h L1454-L1477 templatetypename derived_type void internal_forward_task_impl(buffer_operation *op, derived_type* derived) { if (this-my_reserved || !derived-is_item_valid()) { op-status.store(FAILED, ...); // 无元素或被保留:停止转发 ... } // Try forwarding, giving each successor a chance graph_task* last_task nullptr; size_type counter my_successors.size(); for (; counter 0 derived-is_item_valid(); --counter) derived-try_put_and_add_task(last_task); ... if (last_task !counter) { op-status.store(SUCCEEDED, ...); // 有后继接受:本轮成功 } else { op-status.store(FAILED, ...); // 无人接受:元素保留,等待重试 } }见 internal_forward_task_impl。这段循环精确实现了策略表中的两条规则:single-push:循环一旦有后继接受(last_task非空即代表某次推送成功并产生后续任务),元素已被destroy_front()移出缓冲,不再推送给其余后继;若所有后继都拒绝(counter耗尽而last_task为空),状态置为 FAILED,元素保留在缓冲中——这正是 buffering 策略。驱动机制:转发不是同步完成的。每次try_put成功后,节点会生成一个“转发任务”,由 forward_task 在图的任务体系中以 do-while 循环反复尝试,直到把当前所有可转发元素推完。try_put/try_get/register_successor等入口都会调用enqueue_forwarding_task把生成的任务提交给图的执行域,保证消息不会滞留在缓冲中无人搬运。3.4 保留协议与复位除try_get外,queue_node还继承了完整的预留协议(try_reserve/try_release/try_consume),且都针对队头操作——queue_node::internal_reserve检查my_reserved与队头有效性后调用reserve_front(internal_reserve),internal_consume调用consume_front。这与规范“try_get 在节点被保留时返回 false”的契约互为表里:先 reserve 队头的消费者可以独占消费,其他try_get在此期间会失败。节点被graph::reset()时,reset_node会清空底层reservable_item_buffer,可选清掉后继边,并把forwarder_busy复位(buffer_node::reset_node),即规范中“中间状态不保留”的可编程版本。另外,从源码结构看,两个构造函数都调用了fgt_node(CODEPTR(), FLOW_QUEUE_NODE, ...):它向 OneTBB 的 flow-graph 追踪设施登记了节点类型(区别于FLOW_BUFFER_NODE、FLOW_SEQUENCER_NODE等),可用于运行期识别与剖析该节点,这也是为什么源码里set_name为queue_node单独提供了重载(set_name 重载)。4. 与兄弟节点的关系:一张选型对照表规范目录(flow_graph 规范索引)把若干缓冲节点并列组织,它们的差异集中在“取哪个元素”与“如何推给后继”:节点取元素的顺序转发策略典型用途buffer_nodeLIFO(取队尾,最近入队者)single-push只关心吞吐、不关心顺序的缓冲queue_nodeFIFO(取队头,最早入队者)single-push需要保序消费的流水线缓冲priority_queue_node按比较器取优先级最高者single-push按优先级调度,见 priority_queue_node 规范sequencer_node依 FIFO 入队,但按序列函数决定是否放行single-push乱序生产、按序消费overwrite_node只保留最新一条broadcast-push最新值覆盖旧值write_once_node只接受第一条broadcast-push一次性数据源码中这一谱系也清晰可见:sequencer_node直接以queue_nodeT为基类(sequencer_node 定义),在其上叠加一个“序列器”函数,只有当 FIFO 队头元素符合序列条件时才真正向后继转发——可以说sequencer_nodequeue_node 顺序门控;而priority_queue_node则是与queue_node平级的buffer_node子类(priority_queue_node 定义)。选择时只需回答一个问题:下游消费下一条消息的次序由什么决定?时间序选queue_node,优先级选priority_queue_node,逻辑序列号选sequencer_node,不关心顺序选buffer_node。5. 典型用法与扩展 API5.1 基本连接模式queue_node的标准用法是夹在前驱(生产者节点)与后继(消费者节点)之间充当保序缓冲:#include oneapi/tbb/flow_graph.h using namespace oneapi::tbb; void example(graph g) { queue_nodeint q(g); // 属于图 g 的 FIFO 缓冲节点 // 上游把 q 作为接收者、下游把 q 作为发送者接入 // make_edge(producer_node, q); // 上游 try_put(q, x) 或经边推送 // make_edge(q, consumer_node); // 下游自动接收转发,或主动 try_get int v; while (q.try_get(v)) { // 也可由外部主动拉取,FIFO 顺序 consume(v); } }上图示意的语义全部来自已验证的 API:try_put恒成功(入队尾 触发对队头的转发)、try_get在空或保留时返回 false(取队头)。对“消息如何在多个后继间被单推”的完整示例,可参考用户指南 broadcast_or_send 中围绕priority_queue_node的讲解,其机制对queue_node同样适用。5.2 官方样例中的真实用例OneTBB 官方 GSG 文档索引的官方示例列表里,queue_node有一个被点名的实战用例:“使用queue_node、buffer_node和function_node求解装箱(bin packing)问题的方案”(见 GSG 示例索引)。这类“多生产者任务池 顺序/无序缓冲 并行 worker”的拓扑,正是queue_node最典型的应用形态。5.3 扩展构造与预览特性在较新的 OneTBB 特性中,queue_node还有两类扩展入口,仓库文档中均有对应说明:节点集(node_set)构造:支持queue_node(follows(...))/queue_node(precedes(...))风格的构造,在构造时就完成相邻节点的建边,见 节点构造器参考;源码中对应__TBB_PREVIEW_FLOW_GRAPH_NODE_SET预览特性下的queue_node(const node_setArgs... nodes)构造(node_set 构造)。try_put_and_wait:预览特性try_put_and_wait为queue_node增加了阻塞式推送接口,保证消息入队并成功转发,见 try_put_and_wait 参考。使用这些扩展时需注意它们依赖对应的预览特性宏,是否可用取决于所用 OneTBB 版本的特性开关配置。6. 小结:在 mold 仓库中如何查证本文内容本文所有结论均可在仓库内交叉验证,关键路径如下:内容路径queue_node 规范(语义契约,本文主体)third-party/tbb/doc/main/specification/source/flow_graph/queue_node_cls.rst缓冲/转发策略汇总表third-party/tbb/doc/main/specification/source/flow_graph/forwarding_and_buffering.rst实现:queue_node 类定义third-party/tbb/include/oneapi/tbb/flow_graph.h实现:FIFO 弹出(pop_front)与转发循环flow_graph.h、flow_graph.hbuffer_node 规范(对照 LIFO 语义)third-party/tbb/doc/main/specification/source/flow_graph/buffer_node_cls.rst一句话总结:queue_node是 flow_graph 缓冲节点家族中“严格保序”的那一个——try_put永不失败地把消息堆入队尾并尝试从队头单推转发,try_get从队头按 FIFO 取出(空或保留时返回 false);需要“先来先服务”的并行流水线缓冲,选它不会错。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表