具有相同值的并行写入

Parallel writes of a same value

本文关键字:并行      更新时间:2023-10-16

我有一个程序,它产生了多个线程,这些线程可以将完全相同的值写入完全相同的内存位置:

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存储是否足以防止数据竞争?

语言律师回答,[interro.multithread]n3485

21程序的执行包含数据竞赛,如果它在不同的线程中包含两个冲突的操作,其中至少有一个不是原子的,也没有发生在另一个之前。任何此类数据竞赛都会导致未定义的行为。

4两个表达式求值冲突如果其中一个修改内存位置,另一个修改访问或修改相同的存储器位置。


std::vector<std::atomic<int>>std::memory_order_relaxed存储一起使用是否足以防止数据竞争?

是的。这些访问是原子访问,在通过线程连接引入关系之前,会发生。从生成这些工作线程(通过.join同步)的线程中进行的任何后续读取都是安全和定义的。

这是一场数据竞赛,编译器最终会变得足够聪明,如果他们还没有对代码进行错误编译的话。请参阅第2.4节"如何对具有"良性"数据竞争的程序进行错误编译",了解相同值的写入会破坏代码的原因。

实现细节答案:

虽然语言标准将其归类为未定义的行为,但只要你真的在写相同的数据,你就可以感到非常安全。

为什么?硬件将对同一存储单元的访问顺序化。唯一可能出错的是同时写入多个存储单元,因为硬件无法保证对多个单元的访问以相同的方式顺序化。例如,如果一个进程写入0x0000000000000000,而另一个进程则写入0xffffffffffffffff,则硬件可能会决定以不同的方式对不同字节的访问进行顺序化,从而产生类似0x00000000ffffffff的结果。

然而,如果两个进程写入的数据相同,那么两种可能的序列化之间没有明显的差异,结果是确定的。

现代硬件不以逐字节的方式处理内存访问,相反,CPU通过缓存线与主内存通信,内核通常可以通过8字节字与缓存通信。因此,设置一个正确对齐的指针是一个原子操作,可以依靠它来实现无锁定算法。在更强大的原子操作可用之前,Linux内核就已经利用了这一点。C++将其形式化为atomic<>类型,增加了对更高级硬件功能的支持,如读后写、原子增量等。

但是,当然,如果你依赖你的硬件细节,你真的应该在做之前知道你在做什么。否则,坚持使用atomic<>类型等语言功能,以确保正确的操作并避免UB。


@落选者:

问题是而不是标记的[语言律师],答案明确表示"实现细节答案"。这是有意解释UB在节目中会是什么样子在现实生活中。写这个答案是为了用不同的观点来补充公认的答案(我投了赞成票)。