☰
oneTBB concurrent_hash_map 实战:用 count_strings 示例掌握并发哈希表与词频统计
2026/10/8 14:10:12 网站建设 项目流程
  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载

oneTBB(oneAPI Threading Building Blocks)提供了多种并发容器,其中concurrent_hash_map是在多线程下安全执行“查找—更新”操作的标准选择。本篇文章以仓库中examples/concurrent_hash_map目录下的count_strings示例为核心,完整讲解其从构建、运行、命令行参数到源码实现的全过程:你将掌握如何用 CMake 快速构建 oneTBB 示例、如何通过预定义 make 目标一键运行与性能测量,以及concurrent_hash_map的accessor机制、parallel_for搭配与时间测量等关键编程模式,并可直接把同样的写法迁移到自己的词频统计、计数聚合等真实业务中。

示例概述:count_strings 统计文本中的唯一单词

examples/concurrent_hash_map/README.md明确指出:该目录包含concurrent_hash_map容器的代码示例,目前只有一个示例count_strings——并发地将字符串插入concurrent_hash_map容器。示例的目标非常清晰:统计一段文本中每个单词的出现次数,并报告单词总数与唯一单词数。

示例名称描述
count_strings并发地将字符串插入concurrent_hash_map容器

示例的完整说明见 count_strings 的 README,核心实现见 count_strings.cpp。它模拟了一个典型场景:大量"单词"由程序随机生成(模拟英文词频分布),随后多个线程并发地将它们写入哈希表,每个单词作为键(Key),出现次数作为值(Value)。这正是concurrent_hash_map最经典的用法——计数聚合(count aggregation)。

构建示例:两行 CMake 命令

count_strings使用 CMake 构建,count_strings 的 README 给出了最简步骤:

cmake <path_to_example> cmake --build .

其中<path_to_example>指向示例源码目录,即仓库中的examples/concurrent_hash_map/count_strings。第一条命令完成 CMake 配置(会通过 common.cmake 查找本仓库构建出的 oneTBB 库),第二条命令编译生成可执行文件count_strings。

CMakeLists.txt 揭示了构建细节,值得注意的点有:

  • 支持cmake_minimum_required(VERSION 3.5.0...3.31.3)的版本范围写法;
  • 通过include(../../common/cmake/common.cmake)与set_common_project_settings(tbb)复用所有示例通用的构建配置;
  • target_link_libraries(count_strings TBB::tbb Threads::Threads)显式链接 oneTBB 线程库与系统线程库;
  • target_compile_options(count_strings PRIVATE ${TBB_CXX_STD_FLAG})应用 oneTBB 要求的 C++ 标准编译选项。

如果你希望先构建整个 oneTBB 库再用示例验证,可按仓库顶层 README.md 与 INSTALL.md 的流程构建安装 oneTBB,随后在示例目录执行上述两条命令。

运行示例:预定义 make 目标与应用参数

预定义 make 目标

示例 CMake 通过add_execution_target生成了两个便捷目标(见 CMakeLists.txt):

  • make run_count_strings—— 以预定义参数执行示例(ARGS为空,即使用默认参数);
  • make perf_run_count_strings—— 以测量 oneTBB 性能的建议参数执行示例,其参数为auto 10000000 silent(自动线程数 + 1000 万个字符串 + 静默模式)。

从PERF_ARGS auto 10000000 silent可以推断,性能模式会使用平台默认线程数处理一千万规模的字符串,并关闭除耗时以外的所有输出,以获得干净的性能数据。

命令行参数

直接运行编译出的count_strings可执行文件时,用法如下(摘自 README):

count_strings [n-of-threads=value] [n-of-strings=value] [verbose] [silent] [count_collisions] [-h] [n-of-threads [n-of-strings]]

各参数含义:

参数说明
-h打印命令行选项帮助
n-of-threads使用的线程数;支持low[:high]区间形式,low 与可选的 high 为非负整数,或写auto表示使用平台默认线程数
n-of-strings生成的字符串(单词)数量
verbose在屏幕上打印诊断输出(每个单词及其计数)
silent除耗时外不输出任何内容

实际使用示例:

# 使用 4 个线程、100 万个字符串,静默模式 ./count_strings n-of-threads=4 n-of-strings=1000000 silent # 位置参数形式:2 个线程,50 万个字符串 ./count_strings 2 500000 # 线程数区间 + 详细输出:从 1 到 8 个线程依次测量 ./count_strings 1:8 verbose # 自动线程数,1000 万字符串,静默(等价于 perf 目标) ./count_strings auto 10000000 silent

参数解析的源码原理

命令行解析并非手写判断,而是复用 utility.hpp 中通用的utility::parse_cli_arguments与utility::cli_argument_pack。在 count_strings.cpp 的main中:

  • positional_arg(threads, "n-of-threads", ...)与positional_arg(N, "n-of-strings", ...)注册两个位置参数;
  • arg(verbose, "verbose", ...)、arg(silent, "silent", ...)、arg(count_collisions, "count_collisions", ...)注册三个布尔开关。

thread_number_range(同样定义在 utility.hpp)支持解析low[:high[:(+|*|#)step]]形式的区间:+表示线性递增、*表示倍增、#表示"2 的幂阶梯"步进(默认步进即#4),并且auto会替换为range.auto_number_of_threads()的返回值——本示例传入utility::get_default_num_threads,其实现(见 get_default_num_threads.hpp)就是oneapi::tbb::this_task_arena::max_concurrency(),即平台可用的最大并发度。

核心源码解读:accessor 机制与并发计数

关键类型定义

// 字符串类型:使用 oneTBB 的可扩展分配器 typedef std::basic_string<char, std::char_traits<char>, oneapi::tbb::tbb_allocator<char>> MyString; // 并发哈希表:键为字符串,值为出现次数 typedef oneapi::tbb::concurrent_hash_map<MyString, int> StringTable;

代码位于 count_strings.cpp 顶部。使用tbb_allocator分配字符串内存,能让所有字符串的内存分配也走 oneTBB 的可扩展内存分配器,减少堆竞争。

Tally 函数对象:插入与自增

struct Tally { StringTable& table; Tally(StringTable& table_) : table(table_) {} void operator()(const oneapi::tbb::blocked_range<MyString*> range) const { for (MyString* p = range.begin(); p != range.end(); ++p) { StringTable::accessor a; table.insert(a, *p); a->second += 1; } } };

这是示例的核心模式:对每个单词调用table.insert(a, *p),insert返回true表示键不存在、已插入新条目;返回false表示键已存在、accessor指向已有条目。随后通过a->second += 1完成并发安全的计数自增。

这里的关键是accessor机制。查看 concurrent_hash_map.h 的声明(约第 771—835 行)可以看到:

  • accessor继承自const_accessor,注释说明它"结合了数据访问、加锁与垃圾回收"(Combines data access, locking, and garbage collection);
  • accessor允许写访问(reference operator*()、pointer operator->()),而const_accessor只允许只读访问;
  • 每个accessor在持有期间会对对应条目加锁,析构时自动释放锁与引用,保证"读改写"(read-modify-write)过程的原子性——这正是concurrent_hash_map与concurrent_unordered_map的重要差异:前者通过 accessor 提供元素级锁语义,适合"查找后更新"的并发模式。

concurrent_hash_map还提供完整的find/insert/erase重载族(支持键、值、迭代器区间、initializer_list 等),详见同一头文件的find(约 1112—1138 行)、insert(约 1140—1236 行)与erase(约 1241—1259 行)。

并行执行与计时

static void CountOccurrences(int nthreads) { StringTable table; oneapi::tbb::tick_count t0 = oneapi::tbb::tick_count::now(); oneapi::tbb::parallel_for( oneapi::tbb::blocked_range<MyString*>(Data, Data + N, 1000), Tally(table)); oneapi::tbb::tick_count t1 = oneapi::tbb::tick_count::now(); // ... 遍历 table,累加 total 并统计 unique ... }
  • blocked_range<MyString*>(Data, Data + N, 1000)把N个字符串切分成粒度约 1000 的连续块,交给parallel_for自动调度;
  • tick_count::now()是 oneTBB 提供的高精度计时 API,(t1 - t0).seconds()给出并行阶段耗时;
  • 结束后遍历table累加出现次数得到total,table.size()即唯一单词数unique,输出形如total = 1000000 unique = 27180 time = 0.046。

线程数控制:global_control

示例通过oneapi::tbb::global_control c(oneapi::tbb::global_control::max_allowed_parallelism, p)精确限制并行度。main中的运行逻辑为:

  • 若显式给出了线程数区间,则从threads.first到threads.last按步进逐个测量并打印threads = N;
  • 若未指定(threads.first == 0),先以max_allowed_parallelism=1执行一次串行运行,再以utility::get_default_num_threads()执行一次自动并行运行——串行与并行对比便于直观感受加速比。

数据生成:模拟英文词频的随机单词

为了让词频统计"更像真实文本",示例自带了一个基于音素频率表的随机单词生成器(CreateData及Vowels/Consonants表,见 count_strings.cpp)。其思路是:

  • 维护元音与辅音字母组合表,每项带rates[3](词首、词中、词尾三个位置的出现权重);
  • 按权重随机选取组合拼成单词,使生成的单词呈现接近自然语言的分布——既有大量重复(高频词),也有不少长尾唯一词;
  • 生成后还会构造一句彩蛋消息打印出来。

这样的数据让count_strings既能展示"同一键多次命中"的并发自增路径,也能展示"唯一键大量插入"的扩容路径,比均匀随机数据更贴近实际负载。

可选诊断:统计哈希碰撞

示例还提供了一个可选开关count_collisions:启用后,会对表中每个唯一键的哈希值取掩码(std::hash<MyString>()(i->first) & 0xFFFF)统计分布并打印hashes = N collisions = M。源码注释特别提醒:该统计并不反映哈希表内部真实桶碰撞(it doesn't count real collisions in hash_map, a mask should be applied on hash value),仅作为哈希值分布的粗略诊断工具。它是std::map<std::size_t, int> hashes与全局计数c实现的,每次测量后会被清空,因此只适合小规模定性观察。

更多验证途径

  • 想深入验证concurrent_hash_map的全部 API 与语义,可阅读一致性测试 conformance_concurrent_hash_map.cpp;
  • 若要了解容器在并发读写、遍历与扩容等场景下的正确性保证,可在 oneTBB 文档目录 concurrent_hash_map.rst 找到对应的用户指南章节;
  • 官方 API 参考见 include/oneapi/tbb/concurrent_hash_map.h 中的完整类定义与注释。

小结

count_strings是一个麻雀虽小、五脏俱全的 oneTBB 示例:它用concurrent_hash_map+accessor解决了多线程下的计数聚合问题,用parallel_for+blocked_range实现了负载切分,用tick_count完成高精度计时,用global_control精确控制并行度,并用一套通用 CLI 解析框架统一了参数体验。无论你是要统计日志中的关键词、构建词云,还是做任何"键出现次数"类的并发聚合任务,都可以直接参考 count_strings.cpp 的模式落地。

  • 并发编程
  • 高性能计算

【免费下载链接】oneTBB

oneAPI Threading Building Blocks (oneTBB)

项目地址:https://gitcode.com/gh_mirrors/on/oneTBB
点击查看免费下载

相关推荐

上一篇:华硕主板风扇控制难题全解析:让FanControl完美识别你的硬件
下一篇:Dayspan-Vuetify:为什么它是现代Vue.js应用中最值得投资的日历解决方案?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询