单个加载是否与多个存储同步?

2022-04-18 00:00:00 multithreading atomic c++ c++11

以下引用自C++ Standard - Memory Order:

如果线程A中的原子存储被标记为MEMORY_ORDER_RELEASE,而线程B中来自同一变量的原子加载被标记为MEMORY_ORDER_ACCENTE,则从线程A的角度来看,在原子存储之前发生的所有内存写入(非原子和松弛原子)在线程B中都成为可见的副作用。也就是说,一旦原子加载完成,线程B将确保看到线程A写入内存的所有内容。

仅在释放和获取相同原子变量的线程之间建立同步。其他线程可以看到与其中一个或两个同步线程不同的内存访问顺序。

考虑原子变量v和以下步骤:

  1. 线程A存储在v中使用memory_order_release
  2. 线程B存储在v中使用memory_order_release
  3. 线程C使用memory_order_acquirev加载

以下陈述是否正确: 保证线程C看到线程AB写入内存的所有内容。&q;

编辑: 我将我的评论移到这里是为了更清楚地说明这一点。

我上面的C++引号没有提到B必须阅读A所写的内容。它所说的就是AB在同一个变量上释放/获取。这正是我在这3个步骤中所做的:AB释放一些东西,而C获得一些东西。规格中的哪里说获取了与上一个版本匹配的内容,而不一定是之前的任何内容?


解决方案

v中的加载与两个存储中写入v.load()返回的值的任何一个同步。

标准本身使这一点更加明确。参见n3337原子。顺序p2:对原子对象M执行释放操作的原子操作A与对M执行获取操作的原子操作B同步,并从以A为首的释放序列中的任何副作用中获取其值。

为了说明这一点,这里有一个示例:

int a,b;
std::atomic<int> v = 0;

void thread_A() {
    a = 42;
    v.store(10, std::memory_order_release);
}

void thread_B() {
    b = 17;
    v.store(20, std::memory_order_release);
}

void thread_C() {
    switch (v.load(std::memory_order_acquire)) {
    case 10:
        // thread A must have done this store
        std::cout << a; // ok, prints 42
        std::cout << b; // UB, data race
        break;
    case 20:
        // thread B must have done this store
        std::cout << a; // UB, data race
        std::cout << b; // ok, prints 17
        break;
    case 0:
        // neither A or B has done its store
        std::cout << a; // UB, data race
        std::cout << b; // UB, data race
        break;
    }
}

因此,如果线程C中的v.load()返回10,我们从程序的逻辑中知道该值一定是由线程A中的v.store()存储的;我们程序中的其他任何地方都无法做到这一点。由于该存储上的释放顺序,线程A之前进行的所有写入也是可见的。我们可以安全地从非原子变量a读取,并保证获得值42

更正式地说,v.store(10)与返回10的v.load()同步,v.load()cout << a之前排序,所以v.store(10)线程间发生在cout << a(intro.多线程p11)之前。和a = 42是在v.store(10)之前排序的,正如我们所说的线程间在cout << a之前,所以a = 42线程间在cout << a之前;特别是a = 42cout << a(P12)之前,所以没有数据竞争(P21)。而且,a = 42相对于cout << a(P13)是可见的副作用,且a没有其他副作用可见,因此cout << aa的评价值应为a = 42存储的值,即42

但在本例中,由于v.load()返回10而不是20,我们不知道线程B中的v.store()是否已经发生。也许它确实发生了,并且已经被线程A中的存储覆盖,或者它根本就没有发生过。因此,我们无法证明b = 17发生在cout << b之前,反之亦然,因此这是一场数据竞赛,导致未定义行为。

v.load()返回20的情况类似,但情况相反。如果v.load()返回0,则两个存储都没有发生,访问ab都是数据竞争。

如您所见,这仅在线程A和B存储不同的值时才有用。如果我们更改程序,使A和B都执行v.store(10, std::memory_order_release),那么让线程C观察v.load() == 10不会告诉我们这两个线程中的哪一个执行了存储。加载与其中之一同步,但我们不知道是哪一个。因此,在这种情况下,线程C既不能安全地访问a,也不能安全地访问b,因为这两个线程都可能处于数据竞争中。

断章取义的cp首选项文本可能会使它听起来像是仅仅执行v.load(std::memory_order_acquire)操作就会导致线程实际上等待其他线程中的一些或所有其他存储完成,有点像互斥或std::latch。你不会是第一个以这种方式误读的人。但这是没有意义的--毕竟,负担只是一种负担。它返回v在该特定时刻恰好具有的值,而不阻塞或等待来自任何其他线程的任何事件。

另见Why does this cppreference excerpt seem to wrongly suggest that atomics can protect critical sections?

相关文章