正当我以为我已经掌握了原子知识时,我看到了另一篇文章。这是GCC wiki总体摘要下的摘录:
\n -Thread 1- -Thread 2- -Thread 3-\n y.store (20); if (x.load() == 10) { if (y.load() == 10)\n x.store (10); assert (y.load() == 20) assert (x.load() == 10)\n y.store (10)\n }\nRun Code Online (Sandbox Code Playgroud)\n\n\n释放/获取模式只需要所涉及的两个线程同步。这意味着同步值与其他线程不可交换。线程 2 中的断言仍然必须为 true,因为线程 1 和 2 与 x.load() 同步。线程 3 不参与此同步,因此当线程 2 和 3 使用 y.load() 同步时,线程 3 的断言可能会失败。线程 1 和 3 之间没有同步,因此不能为“x”假设任何值。
\n
文章说线程 2 中的断言不会失败,但线程 3 中的断言可能会失败。
\n我觉得这很令人惊讶。这是我的推理链,线程 3 断言不会失败\xe2\x80\x94也许有人可以告诉我哪里错了。
\ny …我正在阅读关于使用bool进行线程控制的问题,并且被@eran的这个答案所吸引:
使用volatile仅在单核上足够,其中所有线程使用相同的缓存.在多核上,如果在一个核上调用stop()而在另一个核上执行run(),则CPU缓存可能需要一些时间来进行同步,这意味着两个核可能会看到两个不同的isRunning_视图.
如果您使用同步机制,它们将确保所有缓存获得相同的值,代价是暂停程序一段时间.性能或正确性对您来说更重要取决于您的实际需求.
我花了一个多小时搜索一些声明,说同步原语强制缓存一致但失败了.我最接近的是维基百科:
关键字volatile不保证内存屏障可以强制执行缓存一致性.
这表明内存屏障确实强制缓存一致性,并且由于一些同步原语是使用内存屏障(同样来自维基百科)实现的,这是一些"证据".
但我不知道是否相信这一点与否,并确保我不会误解它.
有人可以澄清一下吗?
我不明白,release sequence如果我们在下面的例子中有2个线程,为什么会没有问题.我们对原子变量只有2个操作count.count按顺序递减,如输出中所示.
从C++并发在行动由安东尼·威廉姆斯:
我提到你可以
synchronizes-with relationship在一个store原子变量和load另一个线程的原子变量之间得到一个原子变量,即使在和read-modify-write之间有一系列操作,只要所有操作都被适当标记.如果商店标有,或,并且负载标有,或,并且链中的每个操作都加载了前一个操作所写的值,则操作链构成一个释放序列和初始存储(用于或者)或(是)最终负荷.链中的任何原子读 - 修改 - 写操作都可以有任何内存排序(偶数).storeloadmemory_order_releasememory_order_acq_relmemory_order_seq_cstmemory_order_consumememory_order_acquirememory_order_seq_cstsynchronizes-withmemory_order_acquirememory_order_seq_cstdependency-ordered-beforememory_order_consumememory_order_relaxed要查看这意味着什么(发布顺序)及其重要性,请考虑将
atomic<int>其用作共享队列中项目数的计数,如下面的清单所示.处理事情的一种方法是让生成数据的线程将项目存储在共享缓冲区中,然后执行
count.store(number_of_items, memory_order_release)#1以让其他线程知道数据可用.在实际读取共享缓冲区#4之前,消耗队列项的线程可以执行count.fetch_sub(1,memory_ order_acquire)#2以从队列中声明项目.一旦计数变为零,就没有更多的项目,并且线程必须等待#3.
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
#include <mutex>
std::vector<int> queue_data;
std::atomic<int> count;
std::mutex m;
void process(int i)
{
std::lock_guard<std::mutex> …Run Code Online (Sandbox Code Playgroud) 在英特尔优化手册似乎对存储缓冲区的数量存在于处理器的许多地方,但谈判没有谈存储缓冲区的大小.这是公共信息还是商店缓冲区的大小保留为微架构细节?
我正在研究的处理器主要是Broadwell和Skylake,但其他人的信息也不错.
另外,存储缓冲区究竟做了什么?
这个问题是对此的跟进/澄清:
MOV x86 指令是否实现了 C++11 memory_order_release 原子存储?
这表明MOV汇编指令足以在 x86 上执行获取-释放语义。我们不需要LOCK,围栏xchg等。但是,我很难理解这是如何工作的。
英特尔文档第 3A 卷第 8 章指出:
https://software.intel.com/sites/default/files/managed/7c/f1/253668-sdm-vol-3a.pdf
在单处理器(核心)系统中......
- 读取不会与其他读取重新排序。
- 写入不会与较旧的读取重新排序。
- 对内存的写入不会与其他写入重新排序,但以下情况除外:
但这是针对单核的。多核部分似乎没有提到如何强制执行负载:
在多处理器系统中,以下排序原则适用:
- 单个处理器使用与单处理器系统相同的排序原则。
- 所有处理器都以相同的顺序观察单个处理器的写入。
- 来自单个处理器的写入与来自其他处理器的写入无关。
- 记忆排序服从因果关系(记忆排序尊重传递可见性)。
- 除了执行存储的处理器之外的处理器以一致的顺序看到任何两个存储
- 锁定指令具有总顺序。
那么如何才能MOV单独促进获取释放呢?
论文N4455 No Sane Compiler Will Optimize Atomics讨论了编译器可以应用于原子的各种优化。在有关原子的优化部分,对于 seqlock 示例,它提到了在 LLVM 中实现的转换,其中 afetch_add(0, std::memory_order_release)变成了 amfence后跟一个普通负载,而不是通常的lock addor xadd。这个想法是这样避免了对缓存行的独占访问,并且相对便宜。将mfence仍需要无论供给,以防止序限制的StoreLoad重新排序的mov生成的指令。
无论顺序如何,都会为此类操作执行此转换read-don't-modify-write,并为fetch_add(0, memory_order_relaxed).
但是,我想知道这是否合法。C++ 标准在[atomic.order]下明确指出:
原子读-修改-写操作应始终读取在与读-修改-写操作关联的写之前写入的最后一个值(按修改顺序)。
安东尼威廉姆斯之前也注意到了有关 RMW 操作看到“最新”值的事实。
我的问题是:基于原子变量的修改顺序,基于编译器是否发出lock addvsmfence后跟普通加载,线程可以看到的值的行为是否存在差异?这种转换是否可能导致执行 RMW 操作的线程加载比最新值更旧的值?这是否违反了 C++ 内存模型的保证?
对于在原子数据类型(例如std::atomic<uint8_t>)的对象上执行的存储,GCC 生成:
MOV释放存储( )情况下的指令std::memory_order_release,XCHG顺序一致存储情况下的指令( std::memory_order_seq_cst)。当目标架构为 x86_64 时。然而,当是ARM64(AArch64)时,在这两种情况下,GCC都会生成相同的指令,即STLRB. 没有生成其他指令(例如内存屏障),Clang 也会发生同样的情况。这是否意味着这条被描述为具有存储-释放语义的指令实际上也提供了顺序一致性?
例如,如果在两个内核上运行的两个线程将执行STLRB不同内存位置的存储,那么这两个存储的顺序是否唯一?这样所有其他线程都保证遵守相同的顺序吗?
我之所以问,是因为根据这个答案,使用acquire-loads ,不同的线程可能会观察到不同的release-store顺序。为了观察相同的顺序,需要顺序一致性。
现场演示: https: //godbolt.org/z/hajMKnd53
ARMv8.3 引入了新指令:LDAPR。
当 STLR 后跟 LDAR 到不同的地址时,这两个不能重新排序,因此称为 RCsc(释放一致顺序一致)。
当 STLR 后跟 LDAPR 到不同的地址时,这 2 个地址可以重新排序。这称为RCpc(发布一致处理器一致)。
我的问题是PC部分。
PC 是 TSO 的松弛,其中 TSO 是多副本原子,而 PC 是非多副本原子。
ARMv8的内存模型已改进为多副本原子,因为没有供应商创建过非多副本原子微体系结构,这使得内存模型更加复杂。
所以我遇到了矛盾。
关键问题是:每个存储(包括宽松的存储)都是多副本原子的吗?
如果是这样,那么 rcpc 的 PC 部分对我来说没有意义,因为 PC 是非多副本原子的。由于 ARM 过去是非多副本原子的,它是否可能是一个遗留名称?
PC有多种定义;所以也许这就是原因。
考虑这段代码:
std::atomic<int> x{ 0 };
std::atomic<int> y{ 0 };
int a;
int b;
void thread1()
{
//atomic op A
x.store(1, std::memory_order_relaxed);
//fence X
std::atomic_thread_fence(std::memory_order_seq_cst);
//sequenced-before P, thus in SC order X=>P
//atomic op P
a = y.load(std::memory_order_seq_cst);//0
//reads-before(from-read) Q, thus in SC order P=>Q
}
void thread2()
{
//atomic op Q
y.store(1, std::memory_order_seq_cst);
//sequenced-before B, thus in SC order Q=>B
//atomic op B
b = x.load(std::memory_order_seq_cst);
}
int main()
{
std::thread t2(thread2);
std::thread t1(thread1);
t1.join();
t2.join();
assert(a == 1 || …Run Code Online (Sandbox Code Playgroud) ARM允许重新排序加载后续存储,以便以下伪代码:
// CPU 0 | // CPU 1
temp0 = x; | temp1 = y;
y = 1; | x = 1;
可以导致temp0 == temp1 == 1(并且,这在实践中也是可观察到的).我无法理解这是怎么发生的; 似乎有序提交会阻止它(这是我的理解,它存在于几乎所有的OOO处理器中).我的理由是"在提交之前,负载必须具有其值,它在存储之前提交,并且在提交之前,存储的值不会对其他处理器可见."
我猜我的一个假设肯定是错的,并且必须遵循下列之一:
说明不需要提交一路有序.稍后的存储可以安全地提交并在之前的加载之前变得可见,只要在存储提交核心时可以保证先前的加载(以及所有中间指令)不会触发异常,并且加载的地址是保证与商店不同.
负载可以在其值已知之前提交.我不知道如何实现这一点.
商店在提交之前可以显示.也许某个内存缓冲区允许将存储转发到另一个线程的加载,即使负载先前已加入?
还有别的吗?
有许多假设的微体系结构特征可以解释这种行为,但我最好的是那些实际存在于现代弱有序CPU中的那些.