
并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载本文基于 oneTBB 官方用户指南中的 Wavefront 设计模式文档讲解如何对每个数据项的计算依赖于其前驱项计算结果的流水式迭代问题实施并行化。模式的核心是用oneapi::tbb::parallel_for_each配合原子计数器实现并行版本的拓扑排序——从无依赖的原点出发以波前wavefront形式层层推进。读完本文你将掌握原子计数 feeder 的动态任务派发方法、如何处理前驱数未知的依赖、如何用分块聚合agglomeration摊薄调度开销并理解parallel_for_each在 oneTBB 源码中的底层实现机制。模式背景oneTBB 设计模式系列oneTBB 用户指南的 design_patterns 目录 收录了一组常见并行编程模式Wavefront、Divide_and_Conquer、Reduction、Elementwise 等每个模式统一按Problem问题→ Context上下文→ Forces驱动力→ Solution解决方案→ Example示例的格式组织。Wavefront 是其中处理依赖图驱动计算的代表性模式其命名与实现思路源自 Eun-Gyu Kim 与 Mark Snir 的并行模式研究工作。Problem前驱驱动的前向计算对数据集中的每个数据项执行计算而该项的计算结果依赖其前驱项的计算结果。典型场景如动态规划填表、最短路径逐层松弛、图像处理中的光栅扫描等item_i的结果要等它所有直接前驱pred_1...pred_k的结果就绪后才能开始。若按依赖图拓扑序逐项串行计算即使每项内部工作量很小整体也会被依赖链条拖成串行。Context依赖构成有向无环图这类问题的依赖关系天然形成一个有向无环图DAG每个计算项是图中的一个节点边表示前驱结果 → 后继使用。只要没有环就存在拓扑序也就存在并行化空间——不同分支上的项可以同时计算只要不违反依赖。Forces模式适用的两个前提采用 Wavefront 模式需要满足两个关键约束依赖约束构成有向无环图这是并行拓扑排序可行的基础否则会陷入死锁或需要额外处理环。每个项的直接前驱数量在计算前已知或能在最后一个前驱完成前的某个时刻确定因为每个项需要把原子计数器初始化为前驱个数只有计数归零才能派发。前驱数未知会破坏这一初始化过程。Solution并行拓扑排序 原子计数器Wavefront 的解法是并行化的拓扑排序核心三步为每个数据项关联一个原子计数器初始值设为该项的前驱个数调用oneapi::tbb::parallel_for_each只处理**计数器为零无前驱**的项——这些项构成波前的前沿每处理完一个项就递减其后继项的计数器若某个后继的计数器递减到零说明其所有前驱均已就绪立即通过feeder把它加入parallel_for_each的工作队列。波前就这样从零依赖的项开始沿依赖图逐层扩散直至所有项处理完毕。parallel_for_each会持续消费 feeder 新增的工作项因此调用不会在初始序列处理完就返回而是等到全部含动态加入的项处理完成才结束。变体一前驱数无法提前确定若某个项的前驱数量只能在运行期获知可把获知前驱数本身视为一个额外的概念前驱先把该项计数器设为 1这个未知数占一位当实际前驱数确定后把该概念前驱视为已完成将计数器调整为真实前驱个数或相应递减计数器归零后再派发该项。这一技巧把信息何时可用统一纳入依赖图表达无需改变整体架构。变体二按块聚合降低计数开销如果对每个数据项单独维护计数器、单独派发任务的开销过大就把数据项聚合成块block在块级别上做波前。块的依赖模式与原数据项相同只是粒度放大——单次原子计数和任务调度可覆盖块内大量数据项调度开销被摊薄。Example最长公共子序列LCS文档以最长公共子序列Longest Common Subsequence, LCS动态规划算法为例。给定字符串x和y长度分别为xlen和ylen其串行内核如下int F[MAX_LEN1][MAX_LEN1]; void SerialLCS( const char* x, size_t xlen, const char* y, size_t ylen ) { for( size_t i1; ixlen; i ) for( size_t j1; jylen; j ) F[i][j] x[i-1]y[j-1] ? F[i-1][j-1]1: max(F[i][j-1],F[i-1][j]); }F[i][j]记录x[0..i-1]与y[0..j-1]的最长公共子序列长度前提是F[0][0..ylen]和F[0..xlen][0]已初始化为零。从公式可见计算F[i][j]依赖三个前驱F[i-1][j]上方、F[i][j-1]左方和F[i-1][j-1]左上方依赖图如下冗余依赖对角线的传递闭包图中那条灰色对角依赖F[i-1][j-1]与F[i][j]之间的直接依赖其实是其他依赖的传递闭包因为F[i-1][j]本身依赖F[i-1][j-1]再经由F[i-1][j] → F[i][j]的依赖来自左上区域的信息已保证到达F[i][j]。因此对并行化而言这条对角依赖是冗余的、可以忽略。文档特别强调通常应尽量剔除冗余依赖因为每条被考虑的依赖都要付出一次原子递减操作的成本——依赖越少原子计数开销越低。粒度控制按块聚合若对每个F[i][j]单独调度代价高得不可接受。文档给出的策略是把元素聚合为连续的N×N块块内串行处理块之间保持与原元素相同的依赖关系只是规模放大。这样调度开销摊销到整个块上。并行内核从原点滚动波前并行代码如下。每个块关联一个原子计数器二维数组Count用于按坐标快速查找各块计数器先初始化计数器再从无任何前驱的原点块出发用parallel_for_each滚动波前const int N 64; std::atomicchar Count[MAX_LEN/N1][MAX_LEN/N1]; void ParallelLCS( const char* x, size_t xlen, const char* y, size_t ylen ) { // Initialize predecessor counts for blocks. size_t m (xlenN-1)/N; size_t n (ylenN-1)/N; for( int i0; im; i ) for( int j0; jn; j ) Count[i][j] (i0)(j0); // Roll the wavefront from the origin. typedef pairsize_t,size_t block; block origin(0,0); oneapi::tbb::parallel_for_each( origin, origin1, { // Extract bounds on block size_t bi b.first; size_t bj b.second; size_t xl N*bi1; size_t xu min(xlN,xlen1); size_t yl N*bj1; size_t yu min(ylN,ylen1); // Process the block for( size_t ixl; ixu; i ) for( size_t jyl; jyu; j ) F[i][j] x[i-1]y[j-1] ? F[i-1][j-1]1: max(F[i][j-1],F[i-1][j]); // Account for successors if( bj1n --Count[bi][bj1]0 ) feeder.add( block(bi,bj1) ); if( bi1m --Count[bi1][bj]0 ) feeder.add( block(bi1,bj) ); } ); }要点拆解计数器初始化Count[i][j] (i0)(j0)表示每个块至多有两个前驱——左邻块(i, j-1)与上邻块(i-1, j)。第一行i0无上邻、第一列j0无左邻因此原点块(0,0)计数为 0天然是波前起点动态派发lambda 体携带oneapi::tbb::feederblock第二参数块处理完后对右邻块和下方块各做一次--Count某块计数归零即调用feeder.add把它投递出去块内串行内层双重循环在单个任务内串行填充N×N区域块与块之间才存在并行终止条件parallel_for_each不会在初始的origin处理完就返回而是等待所有经 feeder 加入的块全部完成。源码级原理parallel_for_each 与 feeder 的实现Wavefront 模式的运行基石是parallel_for_each的动态添加工作项能力其实现位于 include/oneapi/tbb/parallel_for_each.h。公开 API 形态oneapi::tbb::parallel_for_each提供多组重载既支持迭代器区间也支持容器/范围对象还可选传入task_group_contexttemplatetypename Iterator, typename Body void parallel_for_each(Iterator first, Iterator last, const Body body); templatetypename Range, typename Body void parallel_for_each(Range rng, const Body body); templatetypename Range, typename Body void parallel_for_each(const Range rng, const Body body); // 以上各形态均有带 task_group_context 的版本Body 的两种合法形态源码中的概念约束parallel_for_each_bodyinclude/oneapi/tbb/parallel_for_each.h#L44-L46明确 Body 的operator()必须是const限定且可二选一B::operator()( item_type item, feederitem_type feeder ) const; // 可动态加工作 B::operator()( item_type item ) const; // 仅静态遍历若 Body 不接收 feeder 参数feeder_holder特化会传入空指针只有 Body 确实可接受feederItem时才会真正构造feeder_impl见feeder_holder的 SFINAE 特化include/oneapi/tbb/parallel_for_each.h#L453-L475。文档开篇的 LCS 示例正是利用了可接收 feeder的形态。feeder 的动态派发链路feeder是抽象基类include/oneapi/tbb/parallel_for_each.h#L56-L71公开接口只有两个add重载拷贝与移动语义。真正干活的是派生类feeder_impl它的internal_add_copy/internal_add_move会用small_object_allocator创建feeder_item_task并spawn到当前执行上下文include/oneapi/tbb/parallel_for_each.h#L171-L207。也就是说每次feeder.add(item)都会产生一个新任务任务执行时再调用 Body 处理该 item。这正是波前可无限扩展的机制新加入的块以任务形式进入调度器与存量任务一同被各工作线程窃取执行。parallel_for_each通过for_each_root_task系列特化按迭代器类别分流include/oneapi/tbb/parallel_for_each.h#L503-L594输入迭代器每批最多取 4 个元素max_block_size 4由input_block_handling_task批量派发保证不会有两个线程并发操作同一个输入迭代器这一点在 Cook_Until_Done_parallel_do.rst 中有专门讨论parallel_for_each从输入迭代器取工作本身是串行的换取的是与串行程序迭代器定义完全兼容前向迭代器使用forward_block_handling_task按块分发随机访问迭代器直接转交给parallel_forblocked_range处理for_each_root_task的随机访问特化从而获得可扩展的静态分派。工作项的构造性要求parallel_for_each需要把输入元素拷贝进任务内部因此要求item_type可拷贝构造源码注释parallel_for_each_body_req明确列出operator()(const)、可拷贝构造、可析构。conformance 测试 test/conformance/conformance_parallel_for_each.cpp 专门验证了迭代器/范围/带上下文等各重载形态以及parallel_for_each使用std::invoke调用 BodyTEST_CASE(parallel_for_each and std::invoke)。实战要点小结先画依赖图再剔除冗余边只保留不可由其他依赖传递得到的关键依赖每条保留的依赖都对应一次原子操作冗余边会白白增加原子计数开销计数器初始化即波前起点计数为零的项无需等待是天然的并行种子逐层递减计数则自动生成新的前沿前驱数未知时用概念前驱兜底把获知前驱数当成一个前驱信息就绪即视为完成粒度要聚合单元素调度成本高时按块处理块内串行、块间波前调度开销摊薄到块规模动态工作靠 feeder 闭环parallel_for_each会等待所有经feeder.add加入的项完成才返回因此波前可以一直滚动到整张依赖图收敛。References模式命名与部分论述源自 Eun-Gyu Kim 与 Mark Snir 的 Wavefront Pattern文档 Wavefront.rst 末尾引用可结合 oneTBB 仓库中 设计模式总览、parallel_for_each 源码 以及 conformance 测试 继续深入。赞分享并发编程高性能计算【免费下载链接】oneTBBoneAPI Threading Building Blocks (oneTBB)项目地址https://gitcode.com/gh_mirrors/on/oneTBB点击查看免费下载相关推荐oneTBB parallel_for_each 实战基于引用计数与 feeder 的稀疏图并行前序遍历parallel_preorder 示例全解析oneTBB parallel_for_each 实战基于引用计数与 feeder 的稀疏图并行前序遍历parallel_preorder 示例全解析 导开发工具构建工具系统编程抖音无水印下载单条到整页批量保存的操作指南抖音无水印下载单条到整页批量保存的操作指南 douyin downloader 是什么 douyin downloader 是一个抖音无水印下载工具它从抖音网页爬虫CLI超强并行计算使用oneTBB实现Bulk Synchronous Parallel模式超强并行计算使用oneTBB实现Bulk Synchronous Parallel模式 还在为大规模数据处理的同步问题头疼吗一文带你掌握高性能并行编程的核心并发编程高性能计算上一篇提升生成质量LFM2.5-2.6B-mxfp4温度与top_k参数调优指南下一篇Qwen3.6-35B-A3B-FP8开发环境搭建从零开始的完整配置教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考