并行std::for_each修改unordered_map元素值的用法是否合规
结论
你的当前写法不符合C++标准要求,本地测试通过只是主流STL实现下的偶然结果,不属于可移植的正确代码。
问题原因
- C++标准对所有标准库容器的线程安全有统一的强制约定:
对同一个容器实例,只要有任意一个线程在调用它的非const成员函数,其他所有线程对该实例的任何成员函数调用(无论调用的是不是const接口)都构成数据竞争,属于未定义行为。
- 你并行阶段调用的
my_map.at(d) = d+1.0,会匹配到std::unordered_map非const版本的at()成员函数——多个并行线程同时调用同一个非const成员函数,本身就已经违反了上述线程安全规则。 - 你本地测试能跑通,纯粹是因为现有主流STL实现(GCC libstdc++、Clang libc++、MSVC STL)中,在容器不插入、不删除元素、不触发rehash的前提下,
at()的内部实现不会修改容器的任何内部状态(比如桶指针、元素计数、哈希种子这类成员),只是做哈希计算、遍历桶查找key的只读逻辑,所以实际运行时不会触发内存冲突。但这只是实现细节,不是标准承诺的行为,未来STL版本如果给at()加调试统计、访问计数之类的逻辑,你的代码随时可能出问题。 - 你对场景的判断有一半是对的:
unordered_map的元素是pair<const Key, T>类型,只要不增删元素、不修改key、不触发rehash,多个线程同时修改不同元素的T类型value本身是完全合法的,因为不同元素的value是互相独立的对象,修改它不会触碰容器的内部状态,也不会造成元素位置变动。问题只是出在你访问元素时调用了非const的容器接口。
符合标准的修正写法
你要实现并行修改已存在元素的value,可以按照下面的逻辑写,完全符合标准要求:
- 插入元素前先调用
my_map.reserve(N),预留足够的桶空间,避免插入过程中多次rehash;所有元素插入完成后,绝对不要在并行阶段做任何插入、删除操作,保证容器结构完全固定。 - 并行阶段通过const引用访问容器,只调用const版本的成员函数查找元素,拿到元素引用后再修改value部分:
void test() { std::vector<double> vec; constexpr auto N = 1000000; vec.reserve(N); for(auto i=0;i<N;i++) vec.push_back(i*1.0); std::unordered_map<double,double> my_map; my_map.reserve(N); // 预留足够桶,避免插入时rehash for(const auto d: vec) my_map.try_emplace(d,d); // 绑定const引用,后续只调用const成员函数 const auto& const_map = my_map; std::for_each(std::execution::par_unseq,vec.cbegin(),vec.cend(),[&](double d) { // const版本at()是const成员函数,多线程并行调用符合标准线程安全要求 const_cast<double&>(const_map.at(d).second) = d+1.0; }); auto total=0.0; for(const auto [key,value]: my_map) total+=value; std::cout << total << std::endl; }
这里的const_cast是合法的:因为原本的value对象就是非const的,我们只是去掉了引用上的const限定,没有修改const对象;且修改value不会影响容器的任何内部状态,不存在未定义行为。
- 额外提醒:并行阶段绝对不要调用
unordered_map::operator[],这个接口是非const的,且key不存在时会自动插入元素触发容器结构修改,无论什么场景下并行调用都会出问题。
内容的提问来源于stack exchange,提问作者user2164703
相关产品推荐
相关产品推荐

