其他线程是不是总是以相同的顺序看到不同线程中不同位置的两次原子写入?

Posted

技术标签:

【中文标题】其他线程是不是总是以相同的顺序看到不同线程中不同位置的两次原子写入?【英文标题】:Will two atomic writes to different locations in different threads always be seen in the same order by other threads?其他线程是否总是以相同的顺序看到不同线程中不同位置的两次原子写入? 【发布时间】:2015-03-04 14:51:50 【问题描述】:

类似于我的previous 问题,请考虑此代码

-- Initially --
std::atomic<int> x0;
std::atomic<int> y0;

-- Thread 1 --
x.store(1, std::memory_order_release);

-- Thread 2 --
y.store(2, std::memory_order_release);

-- Thread 3 --
int r1 = x.load(std::memory_order_acquire);   // x first
int r2 = y.load(std::memory_order_acquire);

-- Thread 4 --
int r3 = y.load(std::memory_order_acquire);   // y first
int r4 = x.load(std::memory_order_acquire);

奇怪的结果 r1==1, r2==0r3==2, r4==0 在这种情况下在 C++11 内存模型下可能吗?如果我用std::memory_order_relaxed 替换所有std::memory_order_acq_rel 会怎样?

在 x86 上,这样的结果似乎是被禁止的,请参阅 this SO question,但我一般询问的是 C++11 内存模型。

额外问题:

我们都同意,std::memory_order_seq_cst 在 C++11 中不允许出现奇怪的结果。现在,Herb Sutter 在他著名的atomic&lt;&gt;-weapons talk@42:30 中说std::memory_order_seq_cst 就像std::memory_order_acq_rel 但是 std::memory_order_acquire-loads 可能不会在std::memory_order_release-writes 之前移动。我看不出上面示例中的这个附加约束如何阻止奇怪的结果。谁能解释一下?

【问题讨论】:

如果您的代码中没有任何std::memory_order_acq_rel,则更改所有std::memory_order_acq_rel 不会有任何区别。您是否遗漏了一些相关的问题? @hvd 我的意思是std::memory_order_acq_rel 代表std::memory_order_acquirestd::memory_order_release。也许我会改变这个...... 根据 C++ 内存模型,结果肯定是允许的。线程 1 和 2 之间没有排序。您可以想象内存变化以不同的速度传播到不同的内核。同步只是关于如果您读取新值会发生什么。无法保证您读取新值。 @TobiasBrüll 当然,这取决于最终生成的程序集,这当然不能由任何标准保证。 我已经在线程 4 中交换了读取顺序,因为您的原始问题没有多大意义:两个线程都以相同的顺序读取 xy,因此他们可以'不检测以相反顺序发生的写入:您需要交换读取顺序才能做到这一点。正如公认的答案所指出的那样,有一个简单的 seq cst 顺序允许您使用问题的原始形式输入的值。 【参考方案1】:

这种重新排序测试称为 IRIW(Independent Readers,Independent Writers),我们在其中检查两个阅读器是否可以看到同一对商店以不同的顺序出现。相关,可能重复:Acquire/release semantics with 4 threads


正如@MWid 的回答所说,非常弱的 C++11 内存模型并不要求所有线程都同意存储的全局顺序。

此答案将解释一种可能的硬件机制,该机制可能导致线程对存储的全局顺序存在分歧,这在为无锁代码设置测试时可能相关。只是因为如果你喜欢 cpu-architecture1 会很有趣。

有关这些 ISA 的抽象模型,请参阅 A Tutorial Introduction to the ARM and POWER Relaxed Memory Models:ARM 和 POWER 都不能保证所有线程都可以看到一致的全局存储顺序。 实际上在 POWER 芯片上观察到这一点是可能的,在 ARM 上可能在理论上是可能的,但在任何实际实现中可能都不可能。

(其他弱排序的 ISA like Alpha 也允许这种重新排序,我认为。ARM 曾经允许它在纸上进行,但可能没有真正的实现进行这种重新排序。ARMv8 甚至加强了他们的-纸模型即使对于未来的硬件也不允许这样做。)

在计算机科学中,存储同时对所有其他线程可见(因此存在单一全局存储顺序)的机器的术语是“多拷贝原子”或“多拷贝原子”。 x86 和 SPARC 的 TSO 内存模型具有该属性,但 ARM 和 POWER 不需要它。


当前的 SMP 机器使用 MESI 来维护单个一致的缓存域,以便所有内核都具有相同的内存视图。当存储从存储缓冲区提交到 L1d 缓存时,它们将成为全局可见的。此时,来自 任何 其他核心的负载将看到该存储。 所有存储都提交缓存的顺序是单一的,因为 MESI 维护一个单一的一致性域。如果有足够的障碍来阻止本地重新排序,就可以恢复顺序一致性。

商店可以它成为全局可见之前对某些但不是所有其他核心可见。

POWER CPU 使用 Simultaneous MultiThreading (SMT)(超线程的通用术语)在一个物理内核上运行多个逻辑内核。我们关心的内存排序规则是针对线程运行的逻辑内核,而不是物理内核。

我们通常认为加载是从 L1d 获取它们的值,但当从同一个核心重新加载最近的存储并且数据直接从存储缓冲区转发时,情况并非如此。 (存储到加载转发,或 SLF)。负载甚至有可能获得一个在 L1d 中从未出现过的值,并且即使在具有部分 SLF 的强排序 x86 上也永远不会出现。 (请参阅我在Globally Invisible load instructions 上的回答)。

存储缓冲区在存储指令退出之前跟踪推测性存储,但也在非推测性存储从内核的无序执行部分(ROB / ReOrder 缓冲区)退出后缓冲它们。

同一物理内核上的逻辑内核共享一个存储缓冲区。推测性(尚未退役)存储必须对每个逻辑核心保持私有。 (否则,这会将他们的推测结合在一起,并且如果检测到错误推测,则要求两者都回滚。这将破坏 SMT 的部分目的,即在一个线程停止或从分支错误预测中恢复时保持核心忙碌) .

但我们可以让其他逻辑核心窥探存储缓冲区以查找最终肯定会提交到 L1d 缓存的非推测性存储。在他们这样做之前,其他物理内核上的线程无法看到它们,但共享相同物理内核的逻辑内核可以。

(我不确定这正是允许在 POWER 上出现这种奇怪现象的硬件机制,但它是合理的)。

这种机制使存储在 SMT 同级内核全局对所有内核可见之前对它们可见。但它仍然在内核中是本地的,因此这种重新排序可以通过只影响存储缓冲区的屏障来廉价地避免,而不会实际强制内核之间进行任何缓存交互。

(在 ARM/POWER 论文中提出的抽象内存模型将其建模为每个内核都有自己的内存缓存视图,缓存之间的链接可以让它们同步。但在典型的现代物理硬件中,我认为唯一的机制是在 SMT 兄弟之间,而不是在单独的内核之间。)


请注意,x86 根本不允许其他逻辑内核窥探存储缓冲区,因为这会违反 x86 的 TSO 内存模型(通过允许这种奇怪的重新排序)。正如我在What will be used for data exchange between threads are executing on one Core with HT? 上的回答所解释的那样,带有 SMT(英特尔称之为超线程)的英特尔 CPU 会在逻辑内核之间静态划分存储缓冲区。


脚注 1:C++ 或特定 ISA 上的 asm 的抽象模型,是您真正需要知道的关于内存排序推理的全部内容。

没有必要了解硬件细节(并且可能会让您陷入一个陷阱,因为您无法想象某种机制,就认为某事是不可能的)。

【讨论】:

ARM 已决定在 ARMv8 中采用 mulitcopy atomic,并且可能还“在实践中”使用 pre v8 架构,因为我认为从未发生过非多拷贝原子行为。见Simplifying ARM Concurrency: Multicopy-AtomicAxiomatic and Operational Models for ARMv8。我不知道它是否是官方的,但它似乎会发生。 很好的答案!我一直很好奇 ARM 没有全局存储顺序,因为它有一个连贯的缓存。现在这个答案给出了合理的解释。 @zanmato:是的,有时 ISA 会在纸面上留下比真实硬件更弱的保证,从而为未来的设计留下空间来做有趣的事情。 (如果您还没有用完每日投票限制,请不要忘记投票。这样一来,您可以让未来的读者知道,如果他们在搜索问题时按投票排序,这里有一些值得一读的内容。)跨度> 几天前,当我第一次点击这个答案时,我投了赞成票。而这个答案肯定是值得的!【参考方案2】:

问题中更新的1 代码(在线程 4 中交换了 xy 的负载)实际上测试了所有线程是否同意全局存储顺序。

在 C++11 内存模型下,结果 r1==1, r2==0, r3==2, r4==0 是允许的,并且实际上可以在 POWER 上观察到。

在 x86 上,这种结果是不可能的,因为“其他处理器以一致的顺序看到存储”。这种结果在顺序一致的执行中也是不允许的。


脚注 1:这个问题最初让两位读者阅读x,然后阅读y顺序一致的执行是:

-- Initially --
std::atomic<int> x0;
std::atomic<int> y0;

-- Thread 4 --
int r3 = x.load(std::memory_order_acquire);

-- Thread 1 --
x.store(1, std::memory_order_release);

-- Thread 3 --
int r1 = x.load(std::memory_order_acquire);
int r2 = y.load(std::memory_order_acquire);

-- Thread 2 --
y.store(2, std::memory_order_release);

-- Thread 4 --
int r4 = y.load(std::memory_order_acquire);

这导致r1==1, r2==0, r3==0, r4==2。因此,这根本不是奇怪的结果。

为了能够说每个读者看到了不同的商店订单,我们需要他们以相反的顺序阅读,以排除最后一家商店只是被延迟了。

【讨论】:

哇。这对我很有帮助。非常感谢。因为,我现在可以得出结论,奖金问题中提到的附加约束本身确实不足以强制执行顺序一致性。用@yohjp 的话来说:“[这是] 顺序一致性约束的一个方面”。 把它改成std::memory_order_seq_cst怎么样?还会被允许吗? @Ari 不,std::memory_order_seq_cst 不会发生这种情况。答案也是如此。【参考方案3】:

在这种情况下,在 C++11 内存模型下,r1==1, r2==0r3==0, r4==2 是否可能出现奇怪的结果

是的。 C++ 内存模型允许这种奇怪的结果

如果我用std::memory_order_relaxed 替换所有std::memory_order_acq_rel 会怎样?

如果您将所有memory_order_acquirememory_order_release 替换为memory_order_relaxed,则您的代码不会发生任何变化。

std::memory_order_seq_cst 就像std::memory_order_acq_relstd::memory_order_acquire-loads 可能不会在std::memory_order_release-writes 之前移动。 我看不出上面示例中的这个附加约束如何防止奇怪的结果

acquire-loads 在release-writes 之前可能不会移动。”展示了顺序一致性约束的一个方面 (memory_order_seq_cst)。

在 C++ 内存模型中,它只保证 seq_cst 具有 acq_rel 语义,而 all seq_cst 原子访问具有一些“总顺序”,不多也不少。当存在这样的“全序”时,我们无法得到奇怪的结果,因为所有seq_cst 原子访问都像在单线程上以任何交错顺序一样执行。

您的 previous question 处理 单个 原子变量的“一致性”,而这个问题询问 所有 原子变量的“一致性”。 C++ 内存模型保证单个原子变量的直观连贯性,即使是最弱的排序 (relaxed),只要默认排序 (seq_cst),就可以保证不同原子变量的“顺序一致性”。 当您明确使用非seq_cst 排序原子访问时,正如您所指出的那样,结果可能很奇怪。

【讨论】:

感谢您的澄清。但是我对您的“不多也不少”的说法感到有些困惑。 seq_cst-load-and-stores 仍然拥有acq_rel-load-and-stores 的所有保证,对吧? 你引用了这个:“当存在这样的“总顺序”时,我们无法得到奇怪的结果,因为所有 seq_cst 原子访问都像在单线程上以任何交错顺序一样执行”,但是为什么你是说“如果你用 memory_order_relaxed 替换所有 memory_order_acquire 和 memory_order_release,你的代码没有任何改变。”?如果有一些总顺序,那么奇怪的结果不应该发生吗?【参考方案4】:

简短的回答是否定的。标准并没有说它们必须是,因此它们不必是。不管你能不能想象出一种特定的方式来实现这一点。

【讨论】:

以上是关于其他线程是不是总是以相同的顺序看到不同线程中不同位置的两次原子写入?的主要内容,如果未能解决你的问题,请参考以下文章

如何避免两个不同的线程从DB中读取相同的行(Hibernate和Oracle 10g)

异步代码是不是在 UI 线程或新/不同线程中运行以不阻塞 UI?

如何让线程按特定顺序执行?

使用Redis的分布式Java锁

是否会以严格的时间顺序调用 GTK+ 超时回调?

java多线程8.性能与活跃性问题