ARTICLE DETAIL

资讯详情

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

oneapi::tbb::concurrent_hash_map 的 Size 与 Capacity 语义解析:empty / size / max_size 的并发实现与使用要点

oneapi::tbb::concurrent_hash_map 的 Size 与 Capacity 语义解析:empty / size / max_size 的并发实现与使用要点 oneapi::tbb::concurrent_hash_map 的 Size 与 Capacity 语义解析empty / size / max_size 的并发实现与使用要点【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读oneapi::tbb::concurrent_hash_map是 oneAPI Threading Building BlocksTBB即本仓库third-party/tbb/目录下随附的并发容器库提供的无序关联容器支持多线程并发插入、查找与删除。本文聚焦该容器尺寸与容量一族接口——empty()、size()与max_size()结合规范文档、头文件实现与一致性测试讲清三者的返回值语义、并发环境下的弱一致性行为、底层原子计数器实现以及在实际并发编程中如何正确使用这些接口。一、接口全景三个成员函数的规范定义该主题对应的规范文档位于 size_and_capacity.rst它定义了concurrent_hash_map类模板中三个与尺寸、容量相关的成员函数声明如下// Defined in header oneapi/tbb/concurrent_hash_map.h bool empty() const; size_type size() const; size_type max_size() const;其中size_type在类模板中定义为实现定义的implementation-defined无符号整数类型见 concurrent_hash_map_cls.rst 的类模板概要实际实现中为std::size_t。这三个函数均声明为const可以安全地在任意线程上对同一个容器实例调用无需持有访问器accessor锁也不会阻塞正在进行的插入、删除操作。empty()容器是否为空bool empty() const;返回值容器为空时返回true否则返回false。并发语义规范文档特别强调——在有并发的插入或删除操作尚未完成pending时返回值可能与容器的实际状态不一致。size()当前元素数量size_type size() const;返回值容器中元素的个数键值对数量键唯一。并发语义与empty()相同当其他线程正在插入或删除元素时返回值可能滞后或超前于此刻的真实状态规范明确允许这种偏差。max_size()容量上限size_type max_size() const;返回值容器理论上可以容纳的最大元素数量。含义这是一个理论上的上界由底层内存分配器的能力决定而非当前哈希表已配置的桶bucket数量。实际能否达到该上限取决于运行时的物理内存与分配器约束。关联阅读与容量直接相关的另一个接口是bucket_count()与rehash()它们属于哈希策略Hash policy小节见 hash_policy.rst。size()反映已存放的元素个数而bucket_count()反映当前哈希桶数量二者不要混淆。二、源码级验证三个函数在头文件中的真实实现规范文档只给出接口契约真正能回答为什么并发下结果可能不一致的是实现。三个函数的实现都位于 concurrent_hash_map.h约第 1070–1079 行// Number of items in table. size_type size() const { return this-my_size.load(std::memory_order_acquire); } // True if size()0. __TBB_nodiscard bool empty() const { return size() 0; } // Upper bound on size. size_type max_size() const { return allocator_traits_type::max_size(base_type::get_allocator()); }从中可以提炼出三个关键实现事实empty()不维护独立的标志位它直接等价于size() 0。因此empty()与size()共享同一套并发语义——它们读到的都是同一个原子计数器的快照。size()读取的是原子计数器my_size使用std::memory_order_acquire内存序。这意味着读取操作自身是线程安全的、无锁的、不会与正在进行的修改相互破坏但读到的数值只是某个时刻的快照不代表调用返回瞬间容器的精确状态。max_size()委托给分配器返回值来自std::allocator_traitsAllocator::max_size(allocator)即concurrent_hash_map使用的分配器默认是tbb_allocatorstd::pairconst Key, T所能分配的最大对象数量。因此max_size()的结果取决于分配器类型与系统配置与当前已分配的内存无关。my_size一个被精心隔离的原子计数器从源码结构看my_size在基类hash_map_base中定义并且它的布局经过了性能考量。在 concurrent_hash_map.h 第 355–364 行protected: bucket_allocator_type my_allocator; // Hash mask sum of allocated segment sizes - 1 std::atomichashcode_type my_mask; // Size of container in stored items std::atomicsize_type my_size; // It must be in separate cache line from my_mask due to performance effects // Zero segment bucket my_embedded_segment[embedded_buckets]; // Segment pointers table. Also prevents false sharing between my_mask and my_size segments_table_type my_table;源码注释明确说明了两点my_size必须与my_mask哈希掩码处于不同的缓存行且中间的my_table段指针表起到了**防止伪共享false sharing**的作用。原因是my_mask在每次扩容、查找时被高频读取而my_size在每次插入、删除时被高频读写二者若挤在同一缓存行会在多核竞争下造成严重的缓存行颠簸。这一布局细节解释了为什么size()可以做到无锁且开销极低。计数器的更新点通过检索头文件中所有my_size的读写点可以完整还原计数器的生命周期操作源码位置实现插入新节点第 290 行size_type sz my_size;前缀自增强制保证插入首元素后完成计数并随后依据sz mask判断是否需要扩容按 key 删除第 1414 行this-my_size--;删除链表节点后递减通过 accessor 删除第 1459 行this-my_size--;同上清空容器clear()第 1013 行this-my_size.store(0, std::memory_order_relaxed);直接清零拷贝构造第 1506 行this-my_size.fetch_add(1, std::memory_order_relaxed);逐节点复制时递增移动构造第 340–341 行整体搬移计数器源容器归零交换swap第 320 行原子交换两个计数器这些更新点表明size()的数值由所有修改操作以原子方式共同维护任何时刻读取都是某个已提交或未完全提交的中间值。例如插入操作先递增计数器、再完成桶内链表链接或反过来分阶段进行另一个线程恰好在两步之间调用size()读到的就是偏大或偏小的近似值——这正是规范文档中结果可能不同于实际容器状态这一条款的实现根源。三、并发语义深度解读为什么 size() 不是精确值标准库std::unordered_map的size()是精确的因为它不允许并发修改而concurrent_hash_map的设计目标是让读写线程互不阻塞因此放弃了强一致计数。具体来说并发下的偏差来源于以下场景进行中的插入节点已创建但尚未完成计数或计数已递增但节点尚未对所有读者可见时size()可能比真实值小或大。进行中的删除节点已被摘除但计数尚未递减时size()可能偏大。并发扩容rehash 过程中节点在旧桶与新桶间迁移计数器的更新与迁移不一定是同一个原子步骤。因此规范文档对empty()与size()给出了完全相同的约束说明The result may differ with the actual container state in case of pending concurrent insertions or erasures.在并发的插入或删除尚未完成时结果可能不同于容器的实际状态。对编程实践的三点启示不要用size()判断遍历是否完成在迭代容器包括并行range的同时观察size()得到的只是参考值。遍历是否结束应以迭代器本身为准参考 iterators.rst 与并行迭代一节。empty()适合做低精度快速判断例如在任务分配前判断工作队列是否还有活干。由于它只是size()0的一次原子读代价极小但要接受偶发的不一致必要时配合其他同步机制兜底。max_size()用于容量规划而非运行时判断它回答的是理论最多能装多少与当前元素数、当前桶数无关。若需要判断是否需要扩容应使用哈希策略接口rehash(n)/bucket_count()见 hash_policy.rst。四、一致性测试中的用法验证TBB 自带的一致性测试 conformance_concurrent_hash_map.cpp 展示了这三个接口在测试代码中的标准用法也印证了文档语义templatetypename test_table_type static void CheckTable( const test_table_type x, int n ) { REQUIRE_MESSAGE( x.size()size_t(n), table is different size than expected ); CHECK(x.empty()(n0)); CHECK(x.size()x.max_size()); // ... }这里size()被用于校验操作后的精确元素数测试环境为单线程或无并发竞争empty()与n0等价被验证size() max_size()则作为不变量断言。此外测试中还有CHECK(!test_map.empty());——对用初始化列表构造的{{1, 2}, {2, 4}}容器验证非空CHECK_FAST(b!a.empty());——在find成功与否与accessor::empty()之间建立一致性断言注意此处是 accessor 的empty()用于判断查找结果是否为空语义与容器的empty()不同CHECK(int(v.size()) i);与CHECK(int(v.bucket_count()) j);的组合——验证在指定rehash(j)后元素数保持i不变而桶数不超过j说明size()不受 rehash 影响只反映元素个数。这也提醒使用者容器级empty()/size()与访问器级accessor::empty()/release()是两套不同语义的接口前者回答容器有没有元素后者回答当前访问器是否绑定到了某个节点。五、总结三个接口的对比与选用接口返回语义并发下的行为实现位置concurrent_hash_map.h典型用途empty()容器是否为空可能滞后/超前于真实状态第 1074 行等价于size()0低开销的粗略判断size()当前元素个数原子快照可能不一致第 1071 行my_size.load(acquire)统计、进度上报、调试max_size()理论容量上限恒定不随内容变化第 1077–1079 行委托分配器容量规划、上界判断一句话记住三者的关系empty()是size()0的语法糖size()是原子计数器的一次 acquire 读max_size()是分配器能力的一次询问。在编写多线程程序时只要接受并发下数值是近似快照这一前提这套接口就能以极低的开销为你的并发哈希表提供稳定的尺寸感知能力。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表