【问题标题】:Parallel writes of a same value相同值的并行写入
【发布时间】:2014-04-13 13:21:21
【问题描述】:

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

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&lt;std::atomic&lt;int&gt;&gt; 代替 std::memory_order_relaxed 存储是否足以防止数据竞争?

【问题讨论】:

  • 实际上很容易确定某个东西是否是data-race-UB:如果可以同时执行多个写入但没有读取,那么您就有麻烦了。如果不止一次读取,但没有写入发生,那你很好。如果一次写入和至少一次读取同时发生,你又被搞砸了。简短:(>1 次写入)或(写入+读取)很麻烦。
  • 使用原子,你是安全的。我认为没有理由在这里介绍 UB。

标签: c++ multithreading c++11 race-condition memory-model


【解决方案1】:

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

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

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


使用std::vector&lt;std::atomic&lt;int&gt;&gt; 代替std::memory_order_relaxed 存储是否足以防止数据竞争?

是的。这些访问是原子的,并且通过线程的连接引入了 happens-before 关系。从产生这些工作线程的线程(通过.join 同步)的任何后续读取都是安全且已定义的。

【讨论】:

    【解决方案2】:

    这是一场数据竞赛,编译器最终会变得足够聪明,即使还没有错误编译代码。 请参阅How to miscompile programs with "benign" data races 第 2.4 节了解为什么写入相同值会破坏代码。

    【讨论】:

    • 这是对 dyp 的 Language-lawyer 回答的评论,以表明它确实有后果,但我缺乏必要的声誉。
    【解决方案3】:

    实现详解:

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

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

    但是,如果两个进程写入的数据相同,那么两个可能的序列化之间没有明显的区别,结果是确定性的。

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

    但是,当然,如果您依赖硬件详细信息,那么您确实应该在做之前知道自己在做什么。否则请坚持使用 atomic&lt;&gt; 类型等语言功能,以确保正确操作并避免 UB。


    @Downvoters:

    该问题没有标记为 [language-lawyer],并且答案明确指出“实施详细答案”。有意解释程序中的 UB 在现实生活中会是什么样子。编写此答案是为了补充已接受的答案(我赞成),对问题有不同的看法。

    【讨论】:

    • 你也不能依赖编译器不利用 UB。循环本质上是一个 memzero。谁知道编译器对此做了什么?
    • @usr 好吧,通常可以依靠优化器来理解并发问题。它会使优化变得过于复杂,唯一的影响就是破坏多线程代码。当您假设只有一个线程时,循环中没有 UB 行为。
    • “为什么?硬件对同一个存储单元的访问顺序化。”您有一个知道 OPs 程序将在其上运行的各种硬件的预言机吗?
    • @Casey 不,我不知道,但我知道字节是如何传输到主内存的;我什至对硬件中什么是可行的,什么是不可行的有一点概念。因此,我很确定 OP 的程序永远不会在相同数据的两次并发写入导致未定义行为的硬件上运行。但是,当然,这是实现细节的答案:-)
    猜你喜欢
    • 2019-12-10
    • 2018-06-04
    • 1970-01-01
    • 2017-10-19
    • 2018-03-26
    • 2013-08-18
    • 2018-04-15
    • 2012-06-14
    • 2019-03-15
    相关资源
    最近更新 更多