免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

oneTBB 流图(Flow Graph)中的数据竞争防护:从并发限制到串行化执行

oneTBB 流图(Flow Graph)中的数据竞争防护:从并发限制到串行化执行 oneTBB 流图Flow Graph中的数据竞争防护从并发限制到串行化执行【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文围绕 mold 仓库所捆绑的 oneTBBIntel oneAPI Threading Building Blocks用户指南中的 avoiding_data_races.rst 展开深入讲解流图flow graph编程模型下数据竞争data race的产生机理与消除方法。你将掌握为什么function_node/multifunction_node的并发限制是防止数据竞争的核心机制、unlimited与serial并发级别的语义差异、input_node为何天然串行执行以及如何通过并发上限与缓冲策略组合出既正确又高效的并行图。全文以仓库内真实源码与配套文档为证据可直接对照查阅。一、流图的承诺边界依赖关系与并发限制是“库级保证”oneTBB 流图的核心思想是用**节点node与边edge显式表达数据依赖关系边告诉运行时库谁必须等谁而节点上的并发限制concurrency limit**告诉运行时库同一个节点最多允许多少个实例同时执行。正如 avoiding_data_races.rst 开头所强调的流图中的边显式地表达了你希望库强制执行的依赖关系同样function_node和multifunction_node对象上的并发限制限定了运行时库允许的最大并发调用次数。这些是库所强制执行的边界库并不会自动保护你免受数据竞争的影响。这是一条极其重要的边界声明oneTBB 的流图不会像锁一样替你守护共享数据它只承诺满足依赖关系与遵守并发上限两项约束。共享变量上的读写安全是程序员的职责。理解了这一点才能正确解释后面示例中出现的错误结果。在源码层面并发级别的定义位于 flow_graph.h//! An enumeration the provides the two most common concurrency levels: unlimited and serial enum concurrency { unlimited 0, serial 1 };unlimited 0表示对并发执行实例数不设上限serial 1表示同一时刻只允许一个实例执行。注意它只是一个枚举类型默认并不会被自动套用——它只是构造节点时传给并发限制参数的可选值。二、一个真实的数据竞争示例全局累加器被并发践踏avoiding_data_races.rst 给出了一个典型反例图中没有做任何防护多个节点并发访问同一个全局变量导致结果错误。原文档的完整示例代码如下graph g; int src_count 1; int global_sum 0; int limit 100000; input_node int src( g, - int { if ( src_count limit ) { return src_count; } else { fc.stop(); return int(); } } ); src.activate(); function_node int, int f( g, unlimited, - int { global_sum i; // data race on global_sum return i; } ); make_edge( src, f ); g.wait_for_all(); cout global sum global_sum and closed form limit*(limit1)/2 \n;程序本意是求1 2 ... 100000的闭式结果limit*(limit1)/2。但f被构造为unlimited并发运行时库会尽量让多个f实例并行处理来自src的消息。于是global_sum i这条读-改-写序列可能被多个线程同时执行产生数据竞争——两个线程可能同时读到旧值、各自累加后再写回导致一次累加被覆盖。原文档明确指出如果你运行上面的示例由于数据竞争它计算出的全局和很可能比期望值略小。global_sum的最终值因此变得不确定且几乎总是偏小。这正是库保证并发上限、但不保证数据安全的直接后果。关键点一function_node的构造签名示例中的function_node int, int f( g, unlimited, lambda )对应源码中 flow_graph.h 的构造函数__TBB_NOINLINE_SYM function_node( graph g, size_t concurrency, Body body, ... );第二个参数concurrency就是并发限制它既可以传unlimited/serial这两个枚举值flow_graph.h也可以传任意正整数表示最多允许几个实例并发。multifunction_node采用完全相同的构造形式flow_graph.h__TBB_NOINLINE_SYM multifunction_node(graph g, size_t concurrency, Body body, ...);因此本节讨论的防护方法对二者同样适用。关键点二input_node为何不产生竞争细心的读者会发现src的 lambda 也在更新全局变量src_countreturn src_count为什么它不构成数据竞争原文档给出的解释是由于input_node总是串行执行因此不可能产生竞争。input_node由运行时库反复调用其函数体operator()( flow_control fc )来产生消息这个调用过程在实现上是串行的——同一时刻最多只有一个生产者在推进src_count。所以src_count不存在并发访问自然安全。这一点与 Data_Flow_Graph.rst 中运行时库会重复调用src_body的函数运算符直到体内调用fc.stop()的描述一致生产是串行的消费才可能并行。三、最小修复把unlimited改为serial并发限制 1对于这个简单示例消除数据竞争的办法极其直观既然问题出在f允许并发执行那就禁止它并发。原文档给出的修复方案是在这个简单示例中可以通过将f的允许并发度从 unlimited 改为 1 来避免数据竞争强制每个值由f顺序处理。即把构造行改为function_node int, int f( g, 1, - int { global_sum i; // 现在串行执行无数据竞争 return i; } );serial与字面量1等价因为enum concurrency { unlimited 0, serial 1 }。修复后f在同一时刻至多有一个实例在运行global_sum i的读-改-写序列被串行化最终global_sum必然等于limit*(limit1)/2。需要强调的是这一修复的代价是放弃该节点的并行性——所有输入被逐个顺序处理。如果f是整个图的计算热点串行化可能显著降低吞吐。因此更精细的做法见下一节只对碰共享状态的节点限流让无副作用的节点保持并行。四、正确姿势按节点副作用决定并发级别avoiding_data_races.rst 的核心原则可以推广为一条设计准则有共享状态读写的节点必须限制并发无副作用的纯计算节点可以放开并发。这一模式在配套文档 Data_Flow_Graph.rst 的经典平方/立方/求和示例中有完整示范int 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;该文档给出的设计说明与本文主题完全吻合由于squarer和cuber节点无副作用它们以 unlimited 并发创建。summer节点通过引用更新全局变量sum因此并行执行不安全它被创建为并发限制为 1。可以看到这正是上一节修复策略的正向应用节点行为并发级别理由squarer纯计算v*vunlimited无共享状态可安全并行cuber纯计算v*v*vunlimited无共享状态可安全并行summer更新全局sum1serial写共享变量必须串行化注意summer与squarer、cuber之间通过make_edge建立依赖因此summer串行执行并不会阻塞上游并行——上游两个节点仍可各自并行计算仅在下游汇合处被串行化。这就是流图把并行度与正确性分离管理的精髓并行发生在无依赖、无共享的路径上串行发生在有共享状态的汇合点上。五、进阶防护并发限制 × 缓冲策略queueing / rejecting并发限制控制的是同时执行多少个实例而节点如何对待超出限制的新消息则由第三个模板参数——**缓冲策略graph_buffer_policy**决定。use_concurrency_limits.rst 给出了完整的模板签名template typename Input, typename Output continue_msg, graph_buffer_policy queueing class function_node;queueing默认达到并发上限后仍接受新消息但在内部缓冲排队。rejecting达到并发上限后拒绝新消息让上游如input_node自行处理背压。use_concurrency_limits.rst 中的经典场景是用rejecting节点控制图内大对象在飞数量graph g; int src_count 0; int number_of_objects 0; int max_objects 3; input_node big_object * s( g, - big_object* { if ( src_count M ) { big_object* v new big_object(); src_count; return v; } else { fc.stop(); return nullptr; } } ); s.activate(); function_node big_object *, continue_msg, rejecting f( g, 3, []( big_object *v ) - continue_msg { spin_for(1); delete v; return continue_msg(); } ); make_edge( s, f ); g.wait_for_all();这段代码把防护能力提升到了资源上限层面f的并发限制为3最多同时处理三个big_object策略为rejecting当三个实例都在运行时f会拒绝input_node发来的新消息被拒绝后input_node会暂停调用其函数体把刚创建的对象缓冲在自己内部此时它的src_count不会再前进等待f有空位再继续推送最终整个系统同时存在的big_object最多 4 个3 个在f中处理1 个缓冲在input_node中。这展示了一个比消除数据竞争更广的防护视角并发限制不仅用于串行化共享状态也用于限制资源在飞行数量。配合 use_input_node.rst 中input_node默认以非激活状态构造input_node( graph g, Body body, bool is_activetrue )通常在建完整张图后调用activate()激活的约定input_noderejecting节点构成了一个天然的背压backpressure机制。六、与 mold 项目的关系为什么一个链接器仓库要捆绑 oneTBB 文档本文主题虽出自 oneTBB 用户指南但在当前仓库中它位于third-party/tbb/目录下是 mold 链接器项目以第三方库形式捆绑的 oneTBB 源码树含include/oneapi/tbb/头文件、src/实现与doc/main/tbb_userguide/文档的一部分。也就是说如果你在 mold 的构建环境中使用 oneTBB 的并行原语例如在插件或配套工具中构建并行流水线同一套流图 API 与数据竞争规则就在本仓库的 flow_graph.h 头文件中真实可用阅读本文时可直接对照 avoiding_data_races.rst、Data_Flow_Graph.rst、use_concurrency_limits.rst 与 use_input_node.rst 四份文档交叉验证当你需要把一段共享状态计算如统计、汇总、日志聚合接入流图时请遵循本文的原则先判断节点是否触碰共享状态再决定并发级别最后用缓冲策略控制资源上限。七、实践检查清单把本文讨论的规则浓缩为可直接套用的检查清单默认假设不安全只要节点体body捕获了外部引用[]并写入共享变量就必须假设它会在多线程下并发执行。共享状态 → 并发限制 1对更新全局/捕获变量的节点使用function_node( g, 1, ... )或serial。纯计算 → unlimited无副作用的节点如v*v可以放心使用unlimited保持并行吞吐。input_node天然串行它的函数体总是串行推进更新其自身计数器是安全的但注意它只保证生产串行不保证下游消费者安全。资源上限 → 并发限制 rejecting需要限制在飞行对象数量时用rejecting策略让input_node背压暂停。结果验证对累加型结果始终与闭式解如limit*(limit1)/2比对用确定性结果暴露隐式竞争。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表