我有一个程序可以生成多个线程,可以将完全相同的值写入完全相同的内存位置:
std::vector<int> vec(32, 1); // Initialize vec with 32 times 1
std::vector<std::thread> threads;
for (int i = 0 ; i < 8 ; ++i) {
threads.emplace_back([&vec]() {
for (std::size_t j = 0 ; j < vec.size() ; ++j) {
vec[j] = 0;
}
});
}
for (auto& thrd: threads) {
thrd.join();
}
在这个简化的代码中,所有线程都可能尝试将完全相同的值写入vec
中的相同内存位置。这是一个数据竞争可能触发未定义的行为,还是安全的,因为在所有线程再次连接之前永远不会读取值?
如果存在潜在的危险数据竞争,那么使用std::vector<std::atomic<int>>
代替std::memory_order_relaxed
商店是否足以阻止数据竞争呢?
答案 0 :(得分:5)
语言律师答案,[intro.multithread] n3485
21 如果程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作, 其中至少有一个不是原子的,也不会发生在另一个之前。任何此类数据竞赛都会产生 未定义的行为。
4 两个表达式评估冲突如果其中一个修改了内存位置而另一个修改了内存位置 访问或修改相同的内存位置。
使用
std::vector<std::atomic<int>>
代替std::memory_order_relaxed
商店而不足以阻止数据竞争?
是。这些访问是原子的,并且通过连接线程引入了发生之前的关系。从产生这些工作者的线程(通过.join
同步)的任何后续读取都是安全和定义的。
答案 1 :(得分:2)
这是一场数据竞争,编译器最终会变得足够智能,如果它们还没有编译错误。 请参阅How to miscompile programs with "benign" data races第2.4节,了解为什么相同值的写入会破坏代码。
答案 2 :(得分:0)
实施细节回答:
虽然语言标准将其归类为未定义的行为,但只要您真正编写相同的数据,就可以真正感到安全。
为什么呢?硬件序列化对同一存储器单元的访问。唯一可能出错的是同时写入多个存储单元,因为硬件无法保证以相同的方式对多个单元的访问进行顺序化。例如,如果一个进程写入0x0000000000000000
,另一个进程写入0xffffffffffffffff
,则您的硬件可能会决定以不同方式对不同字节的访问进行顺序化,从而产生类似0x00000000ffffffff
的内容。
但是,如果两个进程写入的数据相同,则两个可能的序列化之间没有明显的差异,结果是确定性的。
现代硬件不以逐字节方式处理内存访问,而是CPU根据缓存行与主内存进行通信,并且内核通常可以按照8字节字与其缓存进行通信。因此,设置正确对齐的指针是原子操作,可以依赖它来实现无锁算法。在更强大的原子操作可用之前,这已在Linux内核中被利用。 C ++以atomic<>
类型的形式对此进行了形式化,增加了对更高级硬件功能的支持,如读后写,原子增量等。
但是,当然,如果你依赖于你的硬件细节,你真的应该知道你在做什么之前做了什么。否则,请坚持使用atomic<>
类型等语言功能,以确保正确操作并避免使用UB。