TBB concurrent_set 相等性比较语义解析:operator== 与 operator!= 的规范、源码实现与陷阱
TBB concurrent_set 相等性比较语义解析operator 与 operator! 的规范、源码实现与陷阱【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold本文解析 Intel oneAPI Threading Building BlocksTBB本仓库作为第三方依赖位于third-party/tbb中concurrent_set容器的非成员二元比较操作符operator与operator!它们如何定义两个并发容器相等、底层在跳表基类中如何落地实现以及在 C20 环境下operator!的来源变化。读完本文你将掌握并发集合相等性判断的精确语义、实现机制大小预检 迭代器逐一比对、std::equal三参数/四参数重载的差异以及比较操作与并发修改并存时的安全边界。规范原文相等的定义TBB 官方规范non_member_binary_comparisons.rst给出了这两个操作符的完整契约核心是下面这一定义两个oneapi::tbb::concurrent_set对象相等当且仅当它们具有相同的元素个数并且其中一个容器中的每个元素都等于另一个容器中相同位置的元素。也就是说相等判断遵循两层条件元素个数相同size()相等按序逐位相等——由于concurrent_set内部按比较器排序存储相同位置意味着排序后序列的每一位都相等这与 STL 中std::set等关联容器的相等语义一致而不是多重集意义上的元素可任意配对。规范中声明的两个操作符签名为template typename T, typename Compare, typename Allocator bool operator( const concurrent_setT, Compare, Allocator lhs, const concurrent_setT, Compare, Allocator rhs ); // 返回lhs 等于 rhs 时为 true否则为 false template typename T, typename Compare, typename Allocator bool operator!( const concurrent_setT, Compare, Allocator lhs, const concurrent_setT, Compare, Allocator rhs ); // 返回lhs 不等于 rhs 时为 true否则为 false需要注意的一个规范细节见 concurrent_set_cls.rst 的 Non-member functions 一节这些非成员函数所处的确切命名空间是未指定的只要它们能通过相应比较操作被找到即可。实现可以完全依赖参数依赖查找ADL暴露这些函数。本仓库的实现正是采用这种做法类与操作符都定义在内部命名空间tbb::detail::d3中再通过using声明把concurrent_set别名提升到tbb命名空间。容器本体concurrent_set 是跳表的薄封装要理解比较操作符先要理解concurrent_set在源码中的形态。在 concurrent_set.h 中类模板定义为template typename Key, typename Compare std::lessKey, typename Allocator tbb::tbb_allocatorKey class concurrent_set : public concurrent_skip_listset_traitsKey, Compare, geometric_level_generator32, Allocator, false { ... };它继承自concurrent_skip_list定义在 _concurrent_skip_list.h模板参数含义如下模板参数默认值作用Key(规范中写作T)—元素类型value_type与key_type均为它Comparestd::lessT决定容器内排序顺序从而决定相同位置的逐位比较基准Allocatortbb::tbb_allocatorT节点内存分配器从源码结构看concurrent_multiset与concurrent_set复用同一套跳表基类仅通过set_traits中的allow_multimapping布尔开关区分concurrent_set为falseconcurrent_multiset为true。这也解释了为什么相等性比较的语义与 STL 关联容器一致set 语义下元素唯一且有序同位置逐位相等才有明确含义。此外concurrent_set_cls.rst 开头即说明concurrent_set支持并发插入、查找与遍历但不支持并发删除unsafe_erase、unsafe_extract、clear等被明确归类为 Concurrently unsafe modifiers。这一约束直接决定了下一节比较操作的安全边界。源码实现大小预检 std::equaloperator/operator!并不在concurrent_set自身定义而是定义在其基类concurrent_skip_list上位于 _concurrent_skip_list.h#L1270-L1288template typename Traits bool operator( const concurrent_skip_listTraits lhs, const concurrent_skip_listTraits rhs ) { if (lhs.size() ! rhs.size()) return false; // 第一层大小不等快速退出 #if _MSC_VER // MSVC 对 3 参数 std::equal 的 unchecked 迭代器会报警告 // 改用 C14 起的 4 参数重载 return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); #else return std::equal(lhs.begin(), lhs.end(), rhs.begin()); // 第二层逐位比较 #endif } #if !__TBB_CPP20_COMPARISONS_PRESENT template typename Traits bool operator!( const concurrent_skip_listTraits lhs, const concurrent_skip_listTraits rhs ) { return !(lhs rhs); } #endif实现要点可以归纳为三点短路策略先用size()跳表内部的原子计数过滤掉大小不同的情况避免无谓的迭代这是对规范中相同元素个数条件的直接落地。逐位比较委托给std::equal跳表迭代器是前向迭代器ForwardIteratorstd::equal的区间重载可以正常工作。元素相等判断使用value_type自身的operator与Compare比较器无关——即比较语义依赖元素自身的相等性而非排序键的等价关系。编译器差异处理MSVC 分支改用std::equal的四参数重载纯粹是规避编译器对未检查迭代器的告警语义与三参数版本等价。C20 下 operator! 的来源变化注意源码中的条件编译#if !__TBB_CPP20_COMPARISONS_PRESENT在启用 C20 比较库的环境里显式定义的operator!被取消取而代之的是operator三路比较见 _concurrent_skip_list.h#L1290-L1297#if __TBB_CPP20_COMPARISONS_PRESENT __TBB_CPP20_CONCEPTS_PRESENT template typename Traits tbb::detail::synthesized_three_way_resulttypename Traits::value_type operator( const concurrent_skip_listTraits lhs, const concurrent_skip_listTraits rhs ) { return std::lexicographical_compare_three_way(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), tbb::detail::synthesized_three_way_comparator{}); }也就是说C20 环境下a ! b由三路比较运算符按标准改写规则合成行为保持不变而operator、operator等字典序比较则由operator或独立的字典序实现提供规范中另有专章见 non_member_lexicographical_comparisons.rst。同一头文件中还提供非成员swapconcurrent_set.h#L149-L154委托给成员swap。旧头文件路径 tbb/concurrent_set.h 只是一行转发直接#include ../oneapi/tbb/concurrent_set.h两条路径的语义完全一致。实战用法下面这段示例基于仓库测试 test_concurrent_set.cpp 中使用的类型组合std::lessint 自定义分配器演示相等判断的典型用法#include tbb/concurrent_set.h #include vector int main() { tbb::concurrent_setint a, b, c; // 并发插入是安全的多线程同时 insert 同一批元素 // 插入的元素值必须支持线程安全的并发读取 for (int i 0; i 100; i) { a.insert(i); } // 相同内容、相同排序顺序std::less 相等 for (int i 0; i 100; i) { b.insert(i); } // 内容不同 不等 for (int i 0; i 100; i) { c.insert(i * 2); } bool eq (a b); // true size 相同且逐位相等 bool neq (a ! c); // true size 相同时触发逐位比较位置 1 起即不等 // 注意比较是按当前迭代器可见的状态进行的快照式操作 // 遍历期间不要对容器执行 unsafe_erase / clear 等非并发安全修改。 return (eq neq) ? 0 : 1; }几个使用要点比较结果与 Compare 模板参数的间接关系a b本身调用的是元素int的operator但两个容器要逐位对齐前提通常是它们使用等价的排序比较器。若Compare不同导致排序顺序不同即使元素集合相同逐位比较也可能报告不等。与并发操作共存的安全边界规范将concurrent_set的并发安全面限定为插入、查找与遍历。因此在没有任何线程执行unsafe_erase/unsafe_extract/clear/swap的前提下operator与并发insert混合运行是规范允许的比较走前向迭代器与并发插入同样落在 concurrently safe 面内一旦存在并发删除比较行为即无保障。性能特征size()不等时比较是 O(1) 的原子读取size 相等时为 O(n) 线性扫描。对于大容器若你只需要判断是否包含相同元素集合且顺序可控size预检通常能先筛掉绝大多数不相等的情况。验证与测试参考仓库中的 TBB 测试套件为concurrent_set提供了成体系的规格验证可作为语义的权威参照test_concurrent_set.cpp针对 [containers.concurrent_set / concurrent_multiset] 规范的测试其中set_type、greater_set_type等类型别名分别覆盖默认比较器与std::greater逆序比较器场景验证了同一容器族内、按 Compare 排序后逐位比较的语义conformance_concurrent_set.cppC 标准并发容器一致性风格的补充测试。小结concurrent_set的operator/operator!在语义上等价于 STL 关联容器元素个数相同 排序后逐位相等。实现层面位于跳表基类concurrent_skip_list采用原子大小计数预检 std::equal区间比较的简洁策略并在 C20 下将!交由三路比较运算符改写机制处理。使用时的关键约束只有一条比较期间不得有并发非安全修改unsafe_*系列与clear/swap。理解了这两点即可在并行程序中安全地做容器快照一致性校验、断言校验与基准对比。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考