首页 > 解决方案 > 单个负载是否与多个商店同步?

问题描述

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

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

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

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

  1. 使用中的线程A存储vmemory_order_release
  2. 使用中的线程B存储vmemory_order_release
  3. 线程从使用中C加载vmemory_order_acquire

以下陈述是否正确:“线程C保证看到所有线程AB写入内存。”

编辑:我在这里移动我的评论以使这一点更清楚。

我在那里的 C++ 引用没有说B必须阅读A. 它所说的只是在同一个变量上释放/获取AB这就是我在这三个步骤中所做的:释放一些东西,并A获得一些东西。它在规范中的哪里说,获取与最后一个版本匹配,而不一定是在那之前的任何内容?BC

标签: c++multithreadingc++11atomic

解决方案


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

标准本身使这一点更加明确。请参阅n3337 atomics.order 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;
    }
}

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

更正式地说, 与返回 10 的同步,并且在v.store(10) 之前排序因此线程间发生在之前(intro.multithread p11)。And is sequenced before,正如我们所说的线程间发生在之前,所以线程间发生在之前;特别是发生在(p12) 之前,因此没有数据竞争(p21)。此外,现在对于(p13) 来说是一个可见的副作用,并且没有其他副作用可以看到,因此in的评估值v.load()v.load()cout << av.store(10) cout << aa = 42 v.store(10) cout << aa = 42 cout << aa = 42 cout << aa = 42cout << aaacout << a应为 存储的值a = 42,即42

但是在这种情况下,由于v.load()返回的是 10 而不是 20,所以我们不知道v.store()in 线程 B 是否发生了。也许它确实并且已经被线程 A 中的商店覆盖。或者它根本没有发生。所以我们不能证明b = 17 之前发生过 cout << b,反之亦然,因此这是一个导致未定义行为的数据竞争

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

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

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

另请参阅为什么此 cppreference 摘录似乎错误地暗示原子可以保护关键部分?


推荐阅读