
简介这份资源是 Intel Threading Building Blocks 2019 Update 8 面向 Windows 平台的官方开源发行包主要服务使用 C 进行多核并行开发的工程师、学生与项目组用来化解线程创建、任务调度和数据并行的底层复杂度它还附带 Parallel STL使排序、查找等常见容器算法能够自动并行执行。压缩包内共 790 个文件以 h 头文件、cpp 示例源码、lib 静态库以及 dll、pdb 运行调试文件为主另含 vcxproj 与 sln 工程、makefile、CMake 配置和 html 说明文档体积约 33.83MB内容覆盖并行算法、并发容器、任务调度器与内存分配等模块目录划分清晰便于按需检索。已有 435 人学习下载。读者可获得可直接链接的头文件与预编译库并借助示例与工程文件掌握并行 for、sort、reduce 以及并发容器、同步原语的具体写法也能结合变更记录排查版本兼容与编译配置问题对希望把既有单线程代码平稳改造为多线程的开发者具有较高参考价值。1. 从 tbb2019_20190605oss_win.zip 说起Windows 上少写一个线程池能省多少事tbb2019_20190605oss_win.zip 是我在一次 Windows 图像处理模块重构里翻出来的串行跑一遍四十分钟十六核的机器只忙一个核。多数人的第一反应是自己撸一个线程池然后死在任务切分不均、异常传不出来、退出时序对不上这三件事上。TBB 这套东西的价值就在于把这些骨架先给你一个头文件目录配上导入库和运行时 DLLparallel_for、parallel_reduce、task_group直接拿去用不用先证明自己会写工作窃取队列。它适合已经站在 C11 之上、要在 MSVC 工程里加并行但不想引入大型框架的人不适合找跨进程、分布式方案的人也不适合把每个任务都压缩到几微秒的场景。下面按我拆包、接工程、调参数、踩坑的顺序写能抄的地方都给了命令和参数。2. 解压之后别急着编译include、lib、bin 三层怎么对上号2.1 三个目录各管一段生命周期这个包的目录结构不复杂但新手最容易在“我明明加了包含目录还是找不到 tbb/parallel_for.h”上卡半天。原因是三层的职责是分开的include只在编译期被看见lib只在链接期被看见bin只在运行时被看见。任何一层没配平报错的样子都不一样——编译期报 C1083 找不到头文件链接期报 LNK2019 找不到符号运行期直接弹窗说缺 DLL 或者 0xc000007b。位置实际作用生效阶段include\tbb\*.h模板声明与实现全在头里模板代码没有二进制编译lib\intel64\vc14\tbb.lib导入库只有符号桩不是静态库链接lib\...\tbb_debug.lib调试版导入库配/MDd用链接Debugbin\intel64\vc14\tbb.dll真正干活的实现运行tbbmalloc*.lib/tbbmalloc*.dll可选的可伸缩分配器代理版能接管全局 new/delete运行vc14这个目录名值得多说一句。MSVC 从 VS2015 起工具集是 v140之后 v141、v142 一路下来保持了二进制兼容所以一个vc14目录能同时喂给三个版本的 IDE不用为每个 IDE 单独找库。但二进制兼容不等于 CRT 兼容/MD和/MT是两个世界混着来会出那种“编译过了、链接过了、跑起来随机崩”的玄学问题后面第 5 章会专门说。先花十秒确认位数比后面查一小时强:: 看导入库是 x64 还是 x86machine 那行会写 x64 或 x86 dumpbin /headers tbb2019_20190605oss\lib\intel64\vc14\tbb.lib | findstr /i machineintel64目录对应 x64 工程ia32目录对应 Win32 工程两边不能互换。我一般会在解压后立刻把bin\intel64\vc14加进当前命令行会话的PATH只影响这个窗口不污染系统环境变量调试完关掉就干净了。提示想静态链接的话正路是拿源码自己编一份别在解压目录里翻来翻去。目录里给的一般就是导入库配 DLL 的组合具体有什么以你手上这份为准。2.2 为什么在 Windows 上选它而不是 std::thread 加队列手写线程池最难的不是起线程是负载不均。举个具体的一张图按行切给 16 个线程前 15 行都是天空最后一行是密集纹理结果是 15 个线程 200 毫秒就空闲了第 16 个线程跑 3 秒整体加速比连 2 都不到。TBB 的工作窃取在这种场景下能自己找平——某个线程干完了会去别人的队列尾部偷任务你不需要提前知道每块有多重。第二个理由是自动粒度。auto_partitioner会按当前负载动态决定还切不切负载轻的时候少切几次省调度开销负载重的时候继续二分。手写方案里块大小是个常量你只能靠试。第三个理由是组合性。parallel_for的循环体里再套一层parallel_for是安全的两层共享同一个线程池不会出现线程数平方级膨胀。这一点用std::thread做嵌套是灾难很容易在 16 核机器上开出 256 个线程然后集体卡在上下文切换上。代价也要说清楚exe 多一个 DLL 依赖发布时漏拷就起不来API 与包版本绑定网上抄来的新版本写法在这份 2019 的包里根本编不过这是第 5 章第一个坑任务里有阻塞 I/O 时会占着工作线程不放那种场景应该用异步而不是并行需要严格可复现的浮点结果时普通归约每次跑出来都不一样。判断标准很土但好用单次任务总耗时低于几毫秒、或者主体是等网络等磁盘就别上并行收益还不够付调度开销。2.3 二十行跑通第一个并行 for先写一个不依赖任何工程配置的最小例子把“包能用”这件事验证掉再去折腾 IDE。// demo_pf.cpp —— 最小并行 for只验证包能编、能跑、结果对 #include tbb/parallel_for.h #include tbb/blocked_range.h #include vector #include cstdio #include cmath int main() { const size_t n 1u 22; std::vectordouble src(n), dst(n); for (size_t i 0; i n; i) src[i] std::sin(double(i)); // 第三个参数 4096 是 grain size一个任务最少处理 4096 个元素 tbb::parallel_for(tbb::blocked_rangesize_t(0, n, 4096), [](const tbb::blocked_rangesize_t r) { for (size_t i r.begin(); i ! r.end(); i) { dst[i] std::sqrt(std::fabs(src[i])); } }); std::printf(%.6f\n, dst[n / 2]); return 0; }编译和运行:: /I 指向 include 而不是 include\tbb因为代码里写的是 tbb/xxx.h cl /nologo /EHsc /O2 /MD /std:c14 ^ /Itbb2019_20190605oss\include demo_pf.cpp ^ /link /LIBPATH:tbb2019_20190605oss\lib\intel64\vc14 tbb.lib :: 让进程能找到 tbb.dll仅对当前命令行窗口生效 set PATHtbb2019_20190605oss\bin\intel64\vc14;%PATH% demo_pf.exe几个参数不是随便写的。/EHsc必须开TBB 在任务里遇到异常会往上抛没开异常模型的行为是未定义/MD要和 DLL 用的 CRT 家族一致这条后面会反复出现/std:c14是这份包的下限用 C17 也没问题但别用 C20 的协程那套去套它的 API。blocked_range的第三个参数 4096 是初始粒度不是最终粒度auto_partitioner会在这个基础上继续二分所以它更像“上限提示”而不是“固定块大小”。跑通了再看输出如果dst[n/2]是个正常的 0 到 1 之间的数说明头文件、导入库、DLL 三层全部对上了可以进工程配置了。3. 接进 MSVC 与 CMake把包含目录、导入库、DLL 三步配平3.1 VS 属性页里真正要改的只有三处很多人配 TBB 配到怀疑人生是因为在“所有配置”上改了包含目录然后 Debug 配置继承了 Release 的库目录链接到了错误的导入库。正确的做法是先选死Release|x64这一个组合改完再切Debug|x64单独改一次别图省事用“所有配置”。三处分别是C/C → 常规 → 附加包含目录填...\include链接器 → 常规 → 附加库目录填...\lib\intel64\vc14链接器 → 输入 → 附加依赖项Release 填tbb.libDebug 填tbb_debug.lib。就这三处别的地方不用动。工程多了以后手工改容易漏我一般做成一个属性表文件加进属性管理器所有工程共享一份?xml version1.0 encodingutf-8? Project ToolsVersion4.0 xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 PropertyGroup LabelUserMacros TBBRootD:\sdk\tbb2019_20190605oss/TBBRoot /PropertyGroup ItemDefinitionGroup ClCompile AdditionalIncludeDirectories$(TBBRoot)\include;%(AdditionalIncludeDirectories)/AdditionalIncludeDirectories /ClCompile Link AdditionalLibraryDirectories$(TBBRoot)\lib\intel64\vc14;%(AdditionalLibraryDirectories)/AdditionalLibraryDirectories AdditionalDependencies Condition$(Configuration)Debugtbb_debug.lib;%(AdditionalDependencies)/AdditionalDependencies AdditionalDependencies Condition$(Configuration)Releasetbb.lib;%(AdditionalDependencies)/AdditionalDependencies /Link /ItemDefinitionGroup /ProjectTBBRoot抽成宏的好处是换机器只改一行。%(AdditionalIncludeDirectories)这个继承尾巴不能省去掉会把系统自带的包含路径覆盖掉然后vector都找不到。注意不建议在源码里写#pragma comment(lib, tbb.lib)来省事。它在单机小 demo 里很方便但一旦别人换 Debug 配置就会链错库而且库搜索路径还得单独配一遍等于没省。3.2 CMake 里手写一个 imported target不依赖包里带不带 config这份包的 CMake 配置文件在不在、叫什么名字不同来源的包不一样所以我从来不赌它直接手写。稳定、可控出问题也好查。cmake_minimum_required(VERSION 3.15) project(tbb_demo CXX) # 解压目录命令行用 -DTBB_ROOT... 覆盖 set(TBB_ROOT D:/sdk/tbb2019_20190605oss CACHE PATH TBB 解压根目录) set(TBB_ARCH intel64 CACHE STRING intel64 或 ia32) set(TBB_TOOLSET vc14 CACHE STRING 工具集目录名) # NO_DEFAULT_PATH防止系统里另一个 TBB 抢先命中 find_path(TBB_INCLUDE_DIR tbb/parallel_for.h HINTS ${TBB_ROOT}/include NO_DEFAULT_PATH) find_library(TBB_IMPLIB NAMES tbb HINTS ${TBB_ROOT}/lib/${TBB_ARCH}/${TBB_TOOLSET} NO_DEFAULT_PATH) if(NOT TBB_INCLUDE_DIR OR NOT TBB_IMPLIB) message(FATAL_ERROR 没找到 TBB检查 TBB_ROOT${TBB_ROOT}) endif() # Windows 上 SHARED IMPORTED 拆成两个属性位置给 DLL导入库给 .lib add_library(tbb::tbb SHARED IMPORTED) set_target_properties(tbb::tbb PROPERTIES IMPORTED_IMPLIB ${TBB_IMPLIB} IMPORTED_LOCATION ${TBB_ROOT}/bin/${TBB_ARCH}/${TBB_TOOLSET}/tbb.dll INTERFACE_INCLUDE_DIRECTORIES ${TBB_INCLUDE_DIR}) add_executable(demo_pf demo_pf.cpp) target_link_libraries(demo_pf PRIVATE tbb::tbb) target_compile_features(demo_pf PRIVATE cxx_std_14) # 构建完把 DLL 拷到 exe 旁边比改 PATH 可靠 add_custom_command(TARGET demo_pf POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $TARGET_PROPERTY:tbb::tbb,IMPORTED_LOCATION $TARGET_FILE_DIR:demo_pf)关键点在IMPORTED_IMPLIB和IMPORTED_LOCATION的分工前者是链接器吃的.lib后者是运行时那个.dll。只写IMPORTED_LOCATION指向.lib的话链接能过但 CMake 会以为这个库是静态的DLL 就不会被拷。NO_DEFAULT_PATH也别省我之前有个项目在 CI 上死活链错版本最后查出来是系统里有个更早的 TBB 在默认搜索路径上被优先命中了。CMake 3.21 之后可以用$TARGET_RUNTIME_DLLS:demo_pf配合install统一收 DLL团队统一了版本再用那套老项目用上面这种显式拷贝最省心。3.3 DLL 落地三种方式的取舍和 0xc000007b 的判断DLL 找不到这件事在 Windows 上表现得很不一致有时候是明确的“找不到 tbb.dll”有时候直接 0xc000007b应用程序无法正常启动后者其实多半是位数不匹配——64 位 exe 加载了 32 位的 tbb.dll或者反过来。三种部署方式我按场景分开发期用命令行set PATH只影响当前窗口改完立刻生效发布版把tbb.dll拷到 exe 同目录Windows 的搜索顺序里 exe 所在目录优先级很高写在安装包里也简单绝对不要往 System32 里丢那是在给半年后的自己挖坑另一个程序用了别的版本就互相打架。:: 发布前扫一遍依赖确认要带哪些 DLL dumpbin /dependents demo_pf.exe | findstr /i tbb vcruntime msvcp :: 系统里到底有几个同名 DLLPATH 上有几个 where tbb.dll :: 排查时先看加载的到底是哪一份再谈别的 tasklist /m tbb.dlldumpbin /dependents出来的清单就是发布包里必须凑齐的东西。除了 tbb.dll通常还能看到vcruntime140.dll、msvcp140.dll这类 VC 运行时它们不该打包在你的目录里让用户自己装对应的运行时更规范。where tbb.dll能列出搜索路径上所有同名文件如果第一条不是你想要的那份把 PATH 顺序调过来或者干脆走同目录方案绕开这件事。4. 三个骨架写透粒度、结合律、异常传递4.1 parallel_for 的粒度与三种 partitioner粒度是并行 for 里唯一真正需要调的参数。切太细调度开销压过计算四核机器跑出比串行还慢的结果切太粗最后一块变成拖尾加速比上不去。经验值是单个任务 10 微秒到 100 微秒之间对应到具体的循环就是每个任务几千到几万个元素具体数跟你循环体的重量有关。三种 partitioner 的差别值得记一下。默认的auto_partitioner会动态调整绝大多数情况选它simple_partitioner严格按 grain size 切到底适合你已经算准了每块开销均匀的场景也适合做基准测试排除变量static_partitioner在开始前就把区间均分给各个线程省掉窃取开销代价是负载不均时没法自救。#include tbb/parallel_for.h #include tbb/blocked_range.h #include tbb/partitioner.h // 情况一默认自适应grain 只作为初始提示 tbb::parallel_for(tbb::blocked_rangesize_t(0, n, 4096), body); // 情况二每块开销高度一致提前均分减少运行时决策 tbb::parallel_for(tbb::blocked_rangesize_t(0, n), body, tbb::static_partitioner()); // 情况三做基准测试强制固定切分排除自适应带来的抖动 tbb::parallel_for(tbb::blocked_rangesize_t(0, n, 8192), body, tbb::simple_partitioner());有一个常被忽略的优化循环体太轻的时候别去调粒度去合并循环。两层嵌套循环各自开一次parallel_for中间还有一次全量遍历把这两层合成一层一次遍历完省下来的内存带宽比调参数管用得多。4.2 parallel_reduce 的结合律陷阱归约比 for 容易翻车因为它要求你手写的合并操作满足结合律。浮点加法不满足——(ab)c和a(bc)在双精度下结果不同而并行归约的合并顺序每次运行都可能不一样于是同一份输入跑十次出十个结果。这个坑在图像求和、统计均值这类需求里非常常见。标准写法是一个带split构造函数和join的 body#include tbb/parallel_reduce.h #include tbb/blocked_range.h #include cmath struct SumBody { double v 0.0; SumBody() default; // split 构造新 body 从零开始绝对不要拷贝原值 SumBody(SumBody, tbb::split) : v(0.0) {} void operator()(const tbb::blocked_rangesize_t r) { double s v; for (size_t i r.begin(); i ! r.end(); i) s std::sqrt(double(i)); v s; // 累加到局部副本最后统一 join避免频繁写共享变量 } void join(const SumBody rhs) { v rhs.v; } }; double total(size_t n) { SumBody b; tbb::parallel_reduce(tbb::blocked_rangesize_t(0, n, 8192), b); return b.v; }split构造函数里为什么必须清零因为它代表一个全新的、独立的分支累加器拷贝原值等于把已有的部分和重复计算一遍结果直接偏大。operator()里先写局部变量s再一次性赋回v是为了减少成员变量的读写次数编译器更容易把它放在寄存器里。真要可复现的结果用tbb::parallel_deterministic_reduce它保证合并顺序固定代价是并行度受限、可能慢一点。或者干脆把浮点换成定点/整数累加从根上绕开结合律问题。4.3 task_group 与异常怎么从工作线程传出来task_group是这三个里最像“手写线程池”的一个起任务、等任务、取消任务。它的异常语义很干净任务里抛出的异常会被捕获保存在wait()时重新抛出你在主线程 try/catch 就能接住。这一点比自己写的线程池强太多手写的版本里工作线程抛异常基本等于 std::terminate。#include tbb/task_group.h #include stdexcept #include cstdio void run_batch() { tbb::task_group g; g.run([]{ /* 正常任务 */ }); g.run([]{ throw std::runtime_error(worker failed); }); try { g.wait(); // 这里重新抛出工作线程里的异常 } catch (const std::exception e) { std::printf(caught: %s\n, e.what()); // 已经启动的任务无法强停只能等它们自己结束 } }cancel()的语义要理解清楚它只是给还没开始执行的任务打个标记让它们别再跑已经在跑的任务不会被强杀。所以任务体里最好在关键位置检查一下tbb::is_current_task_group_canceling()长循环尤其需要。注意task_group对象析构时如果还有任务没结束行为是未定义的。每个run之后必须有对应的wait或者用析构前的作用域块兜住别把task_group当全局变量用。4.4 线程数与 arena别让 TBB 把机器占满TBB 默认会按逻辑核数起工作线程。这在纯计算的服务里没问题但在带 UI 的桌面程序里会把主线程饿着界面卡顿在容器里如果 CPU 配额限制没被正确识别还可能起一堆线程互相抢。2019 这一版里控制全局并行度的正确入口是global_controltask_scheduler_init已经是弃用状态编过去会有一堆告警#include tbb/global_control.h #include tbb/task_arena.h // 整个进程最多 6 个并行槽位gc 析构后自动恢复原值 tbb::global_control gc(tbb::global_control::max_allowed_parallelism, 6); // 需要隔离的场景这块代码固定 2 个线程不受外面影响 tbb::task_arena arena(2); arena.execute([]{ /* 这里的并行任务最多占 2 个槽位 */ });global_control是栈上对象靠析构恢复原值所以别把它放进一个会提前退出的作用域里否则你以为设了 6实际上早就恢复成 16 了。task_arena适合把相互竞争的两段逻辑隔开比如后台批处理和前台交互各用各的配额避免批处理一启动界面就没响应。5. 五个翻车现场从 LNK2019 到 0xc000007b 的排查顺序5.1 现象链接报一堆tbb::detail::r1::开头的未解析符号原因几乎都是版本错位。网上现在流出来的示例代码大多是 2021 年之后主线版本的写法头文件路径变成oneapi/tbb/parallel_for.h内部命名空间是tbb::detail::r1还带task_arena::constraints、global_control的新重载。这份 2019 的包里根本没有这些符号头文件都在tbb/下面命名空间就是tbb链接自然全红。解决办法不是去改库是把代码改回对应写法包含路径统一成tbb/parallel_for.h这种形式命名空间用tbb::遇到新版才有的 API 就换等价写法——比如新版用constraints限制核数的这里用task_arena(2)或global_control顶上。另一个隐蔽的变体是头文件和 lib 来自两次不同的解压一个人拷了新版头文件、另一个人给了老库同样报这个错。:: 快速确认手上的头文件是哪一代看主版本宏 findstr /i TBB_VERSION_MAJOR TBB_VERSION_MINOR tbb2019_20190605oss\include\tbb\tbb_stddef.h5.2 现象双击 exe 弹 0xc000007b或者提示找不到 tbb.dll前者九成是位数不匹配工程是 x64链接时却指到了ia32目录导入库能链上符号名一样运行时加载 32 位 DLL 直接失败。后者是搜索路径问题DLL 放在了解压目录但没进 PATH也没拷到 exe 旁边。排查顺序我固定这么走先dumpbin /headers看 exe 和 DLL 的 machine 是不是都是 x64再where tbb.dll看搜索路径上第一个命中的是哪份最后tasklist /m tbb.dll看进程实际加载的是哪个绝对路径。三步下来基本没有查不出的。dumpbin /headers demo_pf.exe | findstr /i machine dumpbin /headers tbb2019_20190605oss\bin\intel64\vc14\tbb.dll | findstr /i machine where tbb.dll5.3 现象Debug 配置下随机崩在 delete 上或者报_ITERATOR_DEBUG_LEVEL不匹配原因是 CRT 家族混用。Release 的tbb.dll内部用/MD的堆你的 Debug 工程用/MDd两边各自一套堆管理跨模块释放就是灾难。同类问题还有LNK2038: mismatch detected for _ITERATOR_DEBUG_LEVEL一看就知道一边开了调试迭代器一边没开。解决方式很死板Debug 配置链tbb_debug.lib并且用/MDdRelease 配置链tbb.lib并且用/MD别在 Debug 里图快链 Release 的库。如果工程因为第三方静态库被迫用/MT那这套 DLL 方案就不合适得自己从源码编一份静态版本或者把那个第三方库换掉。5.4 现象加了 parallel_for 反而比串行慢四种原因按出现频率排粒度太细单次迭代只有几十纳秒调度开销占了大头循环体里有std::mutex或者高频atomic自增线程全堵在一个点上假共享多个线程写同一 cache line 上的相邻元素跑在只分了 1 到 2 核的虚拟机上怎么调都没用。对应处理grain size 往上抬两三个数量级先试把单任务做到 10 微秒以上把共享计数器换成线程本地缓冲enumerable_thread_specific或combinable最后合并一次按 cache line 对齐填充别让不同线程写 64 字节内的不同变量先printf一下tbb::this_task_arena::max_concurrency()确认到底有几个槽位可用虚拟机给的核数经常和面板上写的不一致。5.5 现象main 返回后偶发崩溃或卡死DLL 场景尤其明显原因是还有任务没结束进程就开始拆运行时了。典型写法是在某个静态对象的析构里起了task_group或者导出函数里run完没wait就返回宿主进程卸载 DLL 时工作线程还在跑代码段已经没了。我的固定做法是三条所有task_group必须在离开作用域前wait不要在DllMain里碰任何 TBB API加载锁下起线程是死锁的经典配方DLL 导出函数内部自成闭环进去创建、出来之前等干净不把并行状态留给调用方。真需要全局调度器提前收尾的在进程退出的最后一步显式做一次同步别指望析构顺序。6. 一段自检代码确认并行真的没白开调完参数最容易犯的错是“觉得快了”。我的习惯是每次接完一个新库先写一个二十行的基准程序把串行、三种粒度放一起对比跑三遍取中位数。下面这段可以直接改用// bench.cpp —— 同一份核函数串行与三种粒度各跑一遍 #include tbb/parallel_for.h #include tbb/blocked_range.h #include chrono #include cstdio #include cmath #include vector template class F double ms(F f) { auto t0 std::chrono::steady_clock::now(); f(); auto t1 std::chrono::steady_clock::now(); return std::chrono::durationdouble, std::milli(t1 - t0).count(); } int main() { const size_t n 1u 24; std::vectorfloat a(n, 1.0f), b(n, 2.0f), c(n, 0.0f); auto kernel [](size_t grain) { return [, grain] { tbb::parallel_for(tbb::blocked_rangesize_t(0, n, grain), [](const tbb::blocked_rangesize_t r) { for (size_t i r.begin(); i ! r.end(); i) c[i] a[i] * b[i] std::sqrt(a[i]); }); }; }; double base ms([] { for (size_t i 0; i n; i) c[i] a[i] * b[i] std::sqrt(a[i]); }); std::printf(serial %8.2f ms\n, base); for (size_t g : {size_t(64), size_t(4096), size_t(262144)}) { double p a hrefhttps://download.csdn.net/download/buku2010/11438616 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p