C++内存模型中哪些确切规则防止在获取操作前重新排序?

21

我有一个关于以下代码中操作顺序的问题:

std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
  y.exchange(1, std::memory_order_acq_rel);
  r1 = x.load(std::memory_order_relaxed);
}
void thread2() {
  x.exchange(1, std::memory_order_acq_rel);
  r2 = y.load(std::memory_order_relaxed);
}

考虑到cppreference页面上std::memory_order_acquire的描述(https://en.cppreference.com/w/cpp/atomic/memory_order):

此内存顺序下的加载操作执行所影响内存位置上的获取操作:当前线程中没有读取或写入可以在此加载之前重新排序。

看起来很明显,使用thread1thread2并发运行后,永远不可能出现r1 == 0 && r2 == 0的结果。

然而,在C++标准中(我现在正在查看C++14草案),我找不到任何措辞来确保两个松散加载(relaxed loads)不能与获取-释放交换(acquire-release exchanges)重新排序。我错过了什么?

编辑:如评论中所建议的那样,实际上可以得到r1和r2都等于零的情况。 我已更新程序使用load-acquire:

std::atomic<int> x;
std::atomic<int> y;
int r1;
int r2;
void thread1() {
  y.exchange(1, std::memory_order_acq_rel);
  r1 = x.load(std::memory_order_acquire);
}
void thread2() {
  x.exchange(1, std::memory_order_acq_rel);
  r2 = y.load(std::memory_order_acquire);
}

在同时执行 thread1thread2 后,是否可能同时使 r1r2 等于 0?如果不可能,是哪些 C++ 规则阻止了这种情况?


4
确认一下:您想要“语言律师”回答,为什么两个松弛加载仍然被视为读取,并因此受到前面重新排序屏障的约束? - MSalters
2
其实,我觉得我被讨论替换交换的内容搞糊涂了。在这里确实可能看到 r1==0 && r2 == 0,因为 acquire-release(和 consume-release)是成对出现的。只有一个线程获取(和释放)每个原子变量,所以下面的 relaxed load 可能会看到“旧”的值。 - Caleth
2
不,您可以在加载时获取并在交换时释放。 - Caleth
1
不,真的。没有这样的常见变量。试图证明r1==0且r2==0是不可能的。为什么会这样呢? - RbMm
1
https://groups.google.com/forum/#!topic/lock-free/Nescdq-8qVM - RbMm
显示剩余17条评论
6个回答

14
标准并没有通过特定排序参数来定义C++内存模型中原子操作的顺序,而是为获取/释放排序模型定义了形式化的关系,例如“同步”和“先于”,这些关系指定了数据在线程之间如何同步。 N4762,§29.4.2-[atomics.order] 执行释放操作的原子操作A与执行对M的获取操作并从以A为首的释放序列中的任何副作用获取其值的原子操作B同步。
标准还在§6.8.2.1-9中规定,如果存储A与加载B同步,则在A之前发生的任何线程间事件"先于"在B之后发生的任何线程间事件。
您的第二个示例中没有建立“同步”(因此也没有线程间的“先于”)关系,因为运行时关系(检查从加载操作返回的值)不存在。即使您检查了返回值,它也没有用,因为交换操作实际上没有“释放”任何东西(即,在这些操作之前没有排列任何内存操作)。同样,原子负载操作也没有“获取”任何东西,因为在负载操作之后没有排列任何操作。

因此,根据标准,两个示例中负载的四种可能结果(包括0 0)都是有效的。实际上,标准给出的保证不比所有操作上的memory_order_relaxed更强。

如果你想在你的代码中排除0 0的结果,则所有4个操作都必须使用std::memory_order_seq_cst。这将保证所涉及操作的单一全序。


3
在多次重读相关标准的部分后,我觉得这是正确答案。这很不幸,因为它与许多人的信念不同(即获取基本上是LoadLoad + LoadStore,发布是LoadStore + StoreStore,如果仅涉及两个线程,则RMW上的acq_rel等效于seq_cst)。好消息是,对于所有相关的architectures,acq_rel RMW编译成的代码与seq_cst RMW相同。 - Oleg Andreev
1
@OlegAndreev - 你可以执行下面的代码 x.store(1, memory_order_relaxed); atomic_thread_fence(memory_order_acq_rel); r1=y.exchange(1, memory_order_relaxed);y.store(1, memory_order_relaxed); atomic_thread_fence(memory_order_acq_rel); r2 = x.exchange(1, memory_order_relaxed); 在这种情况下可以证明 r1+r2 != 0 或者执行下面的代码 x.exchange(1, memory_order_acq_rel); r1 = y.exchange(1, memory_order_acq_rel)y.exchange(1, memory_order_acq_rel); r2 = x.exchange(1, memory_order_acq_rel); - RbMm
2
@PeterCordes 哦,我主要对ARMv8(aarch64)感兴趣。至少gcc和clang为acq_rel和seq_cst xchg生成了相同的代码:https://godbolt.org/z/uDcqRJ 甚至有些不确定这段代码是否真正是正确的屏障:(http://lists.infradead.org/pipermail/linux-arm-kernel/2014-February/229588.html或https://dev59.com/2WEi5IYBdhLWcg3wMZza vs https://community.arm.com/processors/f/discussions/6558/spin-lock-implementation-for-aarch64----how-to-enforce-acquire-semantics)最终,store(seq_cst)看起来很不错 :) - Oleg Andreev
4
@OlegAndreev 关于你的第一个观点。那些人并没有错。C++委员会通过将线程间行为提升到稍高的抽象层面上做得很好。通过定义这些形式化的关系(sync-with、HB),只要原子操作适合该框架,它们就表现出良好定义的行为。对于不适合该框架的代码(例如你的示例,没有定义的HB),它们没有指定其排序行为,从而允许任何可能的结果。 - LWimsey
3
通过遵循这种方法,C++标准使程序员免于处理混乱的内存重排序问题。 但是从内存重排序的角度来看(获取= #LoadLoad / #LoadStore等),也是完全可以的。即使标准允许0 0结果,基于这些较低级别的属性,实际上几乎不可能出现这种情况。标准没有禁止,只是不想鼓励编写这种代码。 - LWimsey
显示剩余7条评论

6

您已经拥有了关于语言律师部分的答案。但我想回答一个相关问题,即如何理解为什么在使用LL/SC进行RMW原子操作的可能CPU架构上,这是可能的。

C++11禁止这种重排序是没有意义的:在某些CPU架构中,这将需要存储-加载障碍。

实际上,在PowerPC上使用真正的编译器可能是可能的,因为它们将C++11内存顺序映射到汇编指令的方式。

在PowerPC64上,具有acq_rel交换和获取加载(使用指针参数而不是静态变量)的函数使用gcc6.3 -O3 -mregnames编译如下。 这是来自C11版本的,因为我想查看MIPS和SPARC的clang输出,而且Godbolt的clang设置适用于C11 <atomic.h>,但在使用-target sparc64时对于C++11 <atomic>失败。

#include <stdatomic.h>   // This is C11, not C++11, for Godbolt reasons

long foo(_Atomic long *a, _Atomic int *b) {
  atomic_exchange_explicit(b, 1, memory_order_acq_rel);
  //++*a;
  return atomic_load_explicit(a, memory_order_acquire);
}

(源代码和汇编 在Godbolt上提供MIPS32R6、SPARC64、ARM 32和PowerPC64的支持。)

foo:
    lwsync            # with seq_cst exchange this is full sync, not just lwsync
                      # gone if we use exchage with mo_acquire or relaxed
                      # so this barrier is providing release-store ordering
    li %r9,1
.L2:
    lwarx %r10,0,%r4    # load-linked from 0(%r4)
    stwcx. %r9,0,%r4    # store-conditional 0(%r4)
    bne %cr0,.L2        # retry if SC failed
    isync             # missing if we use exchange(1, mo_release) or relaxed

    ld %r3,0(%r3)       # 64-bit load double-word of *a
    cmpw %cr7,%r3,%r3
    bne- %cr7,$+4       # skip over the isync if something about the load? PowerPC is weird
    isync             # make the *a load a load-acquire
    blr

isync不是存储-加载屏障;它只需要先前的指令在本地完成(从乱序部分的核心中退役)。它不等待存储缓冲区被刷新,因此其他线程可以看到较早的存储。

因此,作为交换的一部分的SC (stwcx.)存储可以坐在存储缓冲区中,并在随后的纯获取加载之后成为全局可见的。实际上,另一个Q&A已经问过这个问题,答案是我们认为这种重新排序是可能的。是否`isync`可以防止CPU PowerPC上存储-加载重排序?

如果纯负载是seq_cst,PowerPC64 gcc将在ld之前放置一个sync。使exchange seq_cst不能防止重排序。请记住,C++11仅保证SC操作的单个总顺序,因此对于C++11来保证它,交换和加载都需要是SC。

所以,PowerPC在原子操作方面的C++11到汇编映射有些不寻常。大多数系统在存储器上放置更重的屏障,允许seq-cst加载更加便宜或只在一侧有屏障。我不确定这是否对于PowerPC著名的弱内存排序是必需的,或者是否存在其他选择。 https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html展示了各种架构上的一些可能实现。它提到了ARM的多个替代方案。
在AArch64上,对于线程1的问题的原始C++版本,我们会得到以下结果:
thread1():
    adrp    x0, .LANCHOR0
    mov     w1, 1
    add     x0, x0, :lo12:.LANCHOR0
.L2:
    ldaxr   w2, [x0]            @ load-linked with acquire semantics
    stlxr   w3, w1, [x0]        @ store-conditional with sc-release semantics
    cbnz    w3, .L2             @ retry until exchange succeeds

    add     x1, x0, 8           @ the compiler noticed the variables were next to each other
    ldar    w1, [x1]            @ load-acquire

    str     w1, [x0, 12]        @ r1 = load result
    ret

重新排序无法在此处进行,因为AArch64获取加载与释放存储交互以提供顺序一致性,而不仅仅是普通的acq/rel。释放存储不能与后续获取加载重新排序。(它们可以与后续的普通加载重新排序,在纸上和可能在某些真实硬件中。如果避免在释放存储后立即使用获取加载,则AArch64 seq_cst可能比其他ISA便宜。但不幸的是,这使得acq/rel比x86更糟糕。这通过ARMv8.3-A LDAPR修复,这是一个只有获取没有顺序获取的负载。它允许较早的存储器(甚至STLR)与其重新排序。因此,您只获得acq_rel,允许StoreLoad重新排序但不允许其他重新排序。(它也是ARMv8.2-A的可选功能)。) 在同样或替代具有普通释放LL/SC原子操作的机器上,很容易看出acq_rel无法阻止在交换的LL和SC之后但在不同缓存行上的后续加载变得全局可见。
如果像x86一样使用单个事务实现exchange,那么加载和存储在内存操作的全局顺序中是相邻的,那么肯定没有后续操作可以与acq_rel交换重排序,并且它基本上等同于seq_cst
但是,LL / SC不必是真正的原子事务,即可为该位置提供RMW原子性。
实际上,一个单独的asm swap指令可以具有松散或acq_rel语义。SPARC64需要membar指令围绕其swap指令,因此与x86的xchg不同,它本身不是seq-cst。(与PowerPC相比,SPARC具有非常好/易读的指令助记符。好吧,基本上任何东西都比PowerPC更易读。)
因此,对C++11来说,要求这样做是没有意义的:它会损害在CPU上不需要存储-加载屏障的实现。

谢谢!我不知道AArch64的release-stores是顺序释放的(在ldaxr之前和stlxr之后似乎没有dmb来防止重新排序,这让我感到困惑)。 - Oleg Andreev
1
不错的补充.. 这表明标准委员会真正理解他们正在做什么。 - LWimsey
@OlegAndreev:在假设的ARM上,如果也具有普通发布存储和存储条件,则seq_cst RMW的存储部分将是stlxr(顺序发布),而不是松散存储。如果架构仅具有acq_rel和松散LL / SC,则seq-cst exchange可能会使用松散LL / SC,然后是完整的屏障,因为在每个常规ISA上,包括StoreLoad的任何屏障也包括所有其他屏障。我不知道您所说的哪两个“松散存储”,因为示例具有两个发布存储。 - Peter Cordes
1
@PeterCordes,你关于AArch64的评论“但不幸的是,它使得acq/rel比x86更糟糕,因为它没有更弱的指令来提供只有acq_rel并允许StoreLoad重排序而不是其他重排序”的说法已经过时了。LDAPR和相关指令后来被引入,并且(连同STLR一起)提供了完全的获取/释放语义。 - Daniel Nitzan
1
@DanielNitzan:谢谢,我早就知道那个指令了,但是没有去寻找我之前的答案来抱怨AArch64只有seq_cst。 (我之前还误以为stlr会像x86上的顺序释放存储一样,在后面任何加载之前立即清除存储缓冲区。但实际上除非你紧接着运行ldar,否则情况远不如此糟糕。所以我可能在其他答案中暗示或误导了这种错误想法。如果你发现了一个,请随意编辑和/或让我知道。) - Peter Cordes
显示剩余3条评论

3

为了在两个线程之间创建同步点,我们需要一个原子对象M来实现Release-Acquire ordering。这个M对象在两个操作中是相同的

执行释放操作的原子操作A与执行获取操作并从释放序列中任何副作用中获取其值的M上的原子操作B同步。

更详细地说:

如果线程A中的原子存储标记为memory_order_release,并且线程B中从同一变量读取的原子加载标记为memory_order_acquire,则从线程A的视角看,在原子存储之前发生的所有内存写入(非原子和松散原子)在线程B中成为可见的副作用。也就是说,一旦完成原子加载,线程B保证能够看到线程A写入内存的所有内容。
同步仅在释放和获取相同的原子变量之间建立。
     N = u                |  if (M.load(acquire) == v)    :[B]
[A]: M.store(v, release)  |  assert(N == u)

这里有一个同步点在M的存储释放和加载获取(从存储释放中取值!)。结果是在线程A中将N = u存储(在M上的存储释放之前)在同一M上的加载获取后在BN == u)中可见。
如果以例子来说明:
atomic<int> x, y;
int r1, r2;

void thread_A() {
  y.exchange(1, memory_order_acq_rel);
  r1 = x.load(memory_order_acquire);
}
void thread_B() {
  x.exchange(1, memory_order_acq_rel);
  r2 = y.load(memory_order_acquire);
}

我们可以为常见的原子对象M选择什么?比如说x?如果x.loadx.exchange中加载值,那么x.load(memory_order_acquire);将成为与x.exchange(1, memory_order_acq_rel)同步点(memory_order_acq_rel包括memory_order_release(更强)和exchange包括store)。在获取后,在获取之后的主要同步负载之后(在获取之后的代码中不存在任何内容)与释放之前的存储(但再次在交换之前没有任何代码)。

正确的解决方案(查看几乎完全相同的question)可能是下一个:

atomic<int> x, y;
int r1, r2;

void thread_A()
{
    x.exchange(1, memory_order_acq_rel); // [Ax]
    r1 = y.exchange(1, memory_order_acq_rel); // [Ay]
}

void thread_B()
{
    y.exchange(1, memory_order_acq_rel); // [By]
    r2 = x.exchange(1, memory_order_acq_rel); // [Bx]
}

假设r1 == 0
所有对某个特定原子变量的修改都按照特定于此原子变量的总顺序发生。
我们有两种对y的修改:[Ay][By]。因为r1 == 0,这意味着[Ay]y的总修改顺序中先于[By]。由此得出 - [By]读取由[Ay]存储的值。所以我们得到下面的结果:
  • A写入x - [Ax]
  • A执行store-release将[Ay]存储到y后(acq_rel包括releaseexchange包括store
  • By进行load-acquire([By]的值由[Ay]存储)
  • 一旦原子load-acquire(在y上)完成,线程B保证看到线程A在store-release之前写入内存的所有内容(在y上)。因此它查看[Ax]的副作用 - 并且r2 == 1

另一种可能的解决方案使用 atomic_thread_fence

atomic<int> x, y;
int r1, r2;

void thread_A()
{
    x.store(1, memory_order_relaxed); // [A1]
    atomic_thread_fence(memory_order_acq_rel); // [A2]
    r1 = y.exchange(1, memory_order_relaxed); // [A3]
}

void thread_B()
{
    y.store(1, memory_order_relaxed); // [B1]
    atomic_thread_fence(memory_order_acq_rel); // [B2]
    r2 = x.exchange(1, memory_order_relaxed); // [B3]
}

由于原子变量y的所有修改都是按照总序列发生的,所以[A3]要么在[B1]之前,要么在[B1]之后。

  1. 如果[B1][A3]之前 - [A3]读取由[B1]存储的值 => r1 == 1

  2. 如果[A3][B1]之前 - 则[B1]会读取由[A3]存储的值,并通过Fence-fence synchronization进行同步:

如果线程A中有一个release fence[A2]与线程B中的一个acquire fence[B2]同步,那么它们就会同步,前提条件为:

  • 存在一个原子对象y,
  • 存在一个在线程A中以任何内存顺序修改y的原子写入[A3]
  • [A2]在线程A中先于[A3]发生
  • 存在一个在线程B中以任何内存顺序读取的原子读取[B1]

  • [B1]读取了由[A3]写入的值

  • [B1]在线程B中先于[B2]发生

在这种情况下,所有在线程A中先于[A2]发生的存储([A1])将会先于线程B中在[B2]之后从相同位置(x)进行的所有加载([B3])发生

因此,[A1](将1存储到x)将会在之前并对[B3](从x加载并将结果保存到r2)产生可见效果。因此,将从x加载1,并且r2==1

[A1]: x = 1               |  if (y.load(relaxed) == 1) :[B1]
[A2]: ### release ###     |  ### acquire ###           :[B2]
[A3]: y.store(1, relaxed) |  assert(x == 1)            :[B3]

3
作为一名语言律师,推理难以理解。我想加入一个程序员的角度来解释关于您问题中第二个代码片段的内容。因为这是对称的代码,只需要看一边即可。由于问题涉及到r1(r2)的值,我们从这里开始查看。
r1 = x.load(std::memory_order_acquire);

根据r1的值,我们可以推断其他值的可见性。但由于r1的值没有被测试,获取操作是无关紧要的。无论如何,r1的值可以是任何曾经写入它的值(包括过去和未来的值)。因此它可以是零。不过,我们可以假设它是零,因为我们感兴趣的是整个程序的结果是否为0 0。因此我们可以认为在读取时r1的值为零。
因此,假设我们读取了零,那么如果这个零是由另一个线程在存储释放时写入的,那么该线程对内存的所有其他写操作在存储释放之前也将对此线程可见。然而,我们读取的零是x的初始化值,初始化值是非原子性的,更不用说“释放”了,并且肯定没有任何有序的东西在写入该值到内存之前。所以我们无法判断其他内存位置的可见性。换句话说,再次强调,“获取”操作是无关紧要的。
因此,我们可以得到r1 = 0,并且使用获取操作是无关紧要的。同样的理由也适用于r2。因此,结果可以是r1 = r2 = 0。
事实上,如果您假设在加载获取后r1的值为1,并且该值是由线程2使用"释放"内存顺序写入的(这必须是这种情况,因为只有在x中写入值1的地方),那么我们只知道在线程2之前写入到内存的所有内容也将对线程1可见(前提是线程1读取了x == 1)。但是在写入x之前,线程2没有写任何东西,所以整个释放-获取关系对于加载值为1的情况也是无关紧要的。
*) 然而,可以通过进一步推理显示出由于与内存模型的不一致而某些值永远不可能出现-但这在这里并未发生。

“然而,由于r1的值没有被测试,因此获取是无关紧要的。” 抛弃X的获取(获取负载然后忽略值,如(void)X.load(ack);)仅在程序顺序中没有先前加载X的情况下是无关紧要的,其值未被检查(并且永远不会被检查)。您似乎暗示编译器通常可以忽略(void)X.load(ack);。但是,如果不考虑上下文,编译器不能忽略它。 - curiousguy
不,我说的是“acquire无关紧要”,意思是它没有影响,因为它没有被使用。我的意思是你可以用r1 = x.load(std::memory_model_relaxed)来替换load操作(而不是省略整个load操作!)。如果我的话没有表达清楚,我很抱歉。 - Carlo Wood
“if you read”(如果你读过)我想表达的是,你还没有证明那个定理,它似乎并不普遍。 - curiousguy
明确一点,你在这里的主张是“一次性获取本质上是无用的”。也就是说,(void)X.load(acq); 根据定义是一个 NOP,因此可以被优化掉。对吗? - curiousguy
“初始化值不是原子性的 - 更不用说 '释放' 了 - 而且在将该值写入内存方面,它们前面肯定没有任何“有序”的东西。” 初始化不是原子性的?这到底意味着什么?如果是的话怎么办?你没有看到你正在使用的变量的初始化吗?根据定义,初始化难道不是在我们过去的事情吗? - curiousguy
显示剩余3条评论

1
在原始版本中,可以看到r1 == 0 && r2 == 0,因为没有要求存储传播到其他线程之前进行读取。这不是任何一个线程操作的重新排序,而是例如读取过期缓存。
Thread 1's cache   |   Thread 2's cache
  x == 0;          |     x == 0;
  y == 0;          |     y == 0;

y.exchange(1, std::memory_order_acq_rel); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2

线程1中的释放操作被线程2忽略,反之亦然。在抽象机器中,线程上的x和y值不一致。

Thread 1's cache   |   Thread 2's cache
  x == 0; // stale |     x == 1;
  y == 1;          |     y == 0; // stale

r1 = x.load(std::memory_order_relaxed); // Thread 1
r2 = y.load(std::memory_order_relaxed); // Thread 2

你需要更多的线程才能使用获取/释放对来获得“因果关系违规”,因为正常的排序规则与“变得可见的副作用”规则结合起来,强制至少有一个load看到1。不失一般性,让我们假设线程1先执行。
Thread 1's cache   |   Thread 2's cache
  x == 0;          |     x == 0;
  y == 0;          |     y == 0;

y.exchange(1, std::memory_order_acq_rel); // Thread 1

Thread 1's cache   |   Thread 2's cache
  x == 0;          |     x == 0;
  y == 1;          |     y == 1; // sync 

线程1上的释放与线程2上的获取形成一对,抽象机在两个线程上描述了一致的y

r1 = x.load(std::memory_order_relaxed); // Thread 1
x.exchange(1, std::memory_order_acq_rel); // Thread 2
r2 = y.load(std::memory_order_relaxed); // Thread 2

C++并没有以缓存为术语定义排序,除了http://eel.is/c++draft/intro.races#19中提到的“这实际上使得大多数硬件提供的缓存一致性保证可用于C++原子操作。”因此,如果您想谈论缓存,那么您正在谈论真正的CPU所做的事情。**真正的硬件具有一致的缓存**。通常通过MESI或等效方式维护一致性。存储(或原子RMW)无法在*使线程2的缓存失效*之后更新线程1的缓存。 - Peter Cordes
我们看到的效果是每个核心中的本地StoreLoad重新排序引起的(使用完全障碍的seq_cst将阻止它),而不是不一致的高速缓存。这是一个普遍存在的误解,请不要继续传播。另请参见https://preshing.com/20120515/memory-reordering-caught-in-the-act/和https://software.rajivprab.com/2018/04/29/myths-programmers-believe-about-cpu-caches/。 - Peter Cordes
你所说的“更多线程”/违反因果关系是IRIW试验:不同线程中对不同位置进行的两次原子写入是否总是以相同顺序被其他线程看到? - 大多数真实硬件无法创建这种效果,但一些POWER CPU可以/会这样做。 - Peter Cordes
@PeterCordes 你确定没有硬件不提供缓存一致性保证吗?是的,特定(非常流行)的实现对这些情况有更强的保证,但这是普遍存在的吗? - Caleth
我不能百分之百确定,但我从未听说过一个C++编译器必须实际发出显式刷新指令才能看到其他线程的存储,只有对于不同位置的本地操作之间的屏障。存在具有异构微控制器+ DSP且不具有缓存一致性的ARM硬件,但是C++实现不会在这些核心上运行std::thread线程。(ARM假定单个程序的线程将位于相同的“内部可共享”一致性域中,而这些非一致性核心则不是,尽管它们确实共享内存。) - Peter Cordes
显示剩余2条评论

0

我尝试用其他词语来解释。

假设每个线程同时在不同的CPU核心上运行,线程1在A核心上运行,线程2在B核心上运行。

B核心无法知道A核心中真正的运行顺序。内存序的意义仅仅是为了把A核心的运行结果展示给B核心。

std::atomic<int> x, y;
int r1, r2, var1, var2;
void thread1() { //Core A
  var1 = 99;                                  //(0)
  y.exchange(1, std::memory_order_acq_rel);   //(1)
  r1 = x.load(std::memory_order_acquire);     //(2)
}
void thread2() { //Core B
  var2 = 999;                                 //(2.5)
  x.exchange(1, std::memory_order_acq_rel);   //(3)
  r2 = y.load(std::memory_order_acquire);     //(4)
}

例如,(4) 只是对 (1) 的请求。(其中的代码类似于“具有 memory_order_release 的变量 y”),而在核心 B 中,(4) 应用于 A 的特定顺序:(0)->(1)->(4)。
对于不同的请求,在其他线程中可能会看到不同的序列。(如果现在我们有核心 C 和一些原子变量与核心 A 交互,核心 C 可能会看到与核心 B 不同的结果。)
好的,现在有一个逐步详细的解释:(针对上面的代码)
我们从核心 B 开始:(2.5)
  • (2.5)var2 = 999;

  • (3)acq: 使用'memory_order_release'查找变量'x',没有找到。现在,我们可以猜测核心A中的顺序[(0),(1),(2)]或[(0),(2),(1)]都是合法的,所以我们(B)可以重新排序(3)和(4)。

  • (3)rel: 使用'memory_order_acquire'查找变量'x',找到(2),因此向核心A提供有序显示列表:[var2=999, x.exchange(1)]

  • (4) 使用'memory_order_release'查找变量'y',在(1)找到。所以现在我们站在核心B上,我们可以看到核心显示给我的源代码:'There's must have var1=99 before y.exchange(1)'。

  • 思路是:我们可以看到在y.exchange(1)之前具有var1=99的源代码,因为我们向其他核心发出了请求,并且核心A向我响应了结果。(请求是y.load(std::acquire))。如果还有其他核心也想观察A的源代码,则它们无法得出该结论。

  • 我们永远无法知道(0)(1)(2)的真实运行顺序。

    • A本身的顺序可以确保正确的结果(似乎是单线程)
    • B的请求对A的真实运行顺序也没有任何影响。
  • 这也适用于B (2.5) (3) (4)

也就是说,对于特定核心的操作确实执行了,但没有告诉其他核心,因此“其他核心中的本地缓存”可能是错误的。

因此,在问题代码中存在(0,0)的可能性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接