C++ 记忆模型,load acquire语义实际上是如何工作的?

C++ 记忆模型,load acquire语义实际上是如何工作的?,c++,memory-model,stdatomic,instruction-reordering,C++,Memory Model,Stdatomic,Instruction Reordering,从非常好和关于内存重新排序 Q1:我知道缓存一致性、存储缓冲区和失效队列是内存重新排序的根本原因 存储释放是可以理解的,在将标志设置为true之前,必须等待所有加载和存储完成 关于load acquire,原子负载的典型用法是等待标志。假设我们有两个线程: int x = 0; std::atomic<bool> ready_flag = false; 编辑:在thread-1中,它应该是一个while循环,但我复制了上面文章中的逻辑。所以,假设内存重新排序是及时发生的 Q2:因为

从非常好和关于内存重新排序

Q1:我知道缓存一致性、存储缓冲区和失效队列是内存重新排序的根本原因

存储释放是可以理解的,在将标志设置为true之前,必须等待所有加载和存储完成

关于load acquire,原子负载的典型用法是等待标志。假设我们有两个线程:

int x = 0;
std::atomic<bool> ready_flag = false;
编辑:在thread-1中,它应该是一个while循环,但我复制了上面文章中的逻辑。所以,假设内存重新排序是及时发生的

Q2:因为(1)和(2)取决于if条件,CPU必须等待ready_标志,这是否意味着写释放就足够了?在这种情况下,内存重新排序是如何发生的


<强> q3:显然,我们有“强”>负载“< /强”,所以我猜想MEM重排序是可能的,那么我们应该把栅栏放在哪里,(1)还是(2)?< /P> < P> <强> C++标准没有指定任何特定构造< /强>生成的代码;只有线程通信工具的正确组合才能产生有保证的结果

<> p> C++中的CPU没有得到保证,因为C++不是一种(宏)程序集,甚至不是“高级汇编”,至少在所有对象都不具有易失性类型时也不是这样。 原子对象是线程之间交换数据的通信工具。对于内存操作的正确可视性,正确的使用方法是(至少)释放后加载acquire(获取),两者之间的RMW相同,或者在任何变量上使用(至少)释放(获取)替换RMW(响应负载),并使用放松操作和单独的围栏

在所有情况下:

  • 线程“发布”和“完成”标志必须使用至少释放的内存顺序(即:释放、释放+获取或顺序一致性)
  • 而“订阅”线程,即作用于标志的线程必须至少使用acquire(即:acquire、release+acquire或sequential consistency)

在单独编译代码的实践中,根据CPU的不同,其他模式也可以工作。

访问原子变量不是互斥操作;它仅以原子方式访问存储值,没有任何CPU操作中断访问的机会,因此在访问该值时不会发生数据争用(它还可以对其他访问发出障碍,这是内存顺序提供的)。但就是这样;它不会等待任何特定的值出现在原子变量中

因此,您的
if
语句将读取当时出现的任何值。如果要保护对
x
的访问,直到另一条语句写入并通知原子,则必须:

  • 在原子标志返回值
    true
    之前,不允许从
    x
    读取任何代码。简单地测试一次值并不能做到这一点;您必须在重复访问上循环,直到它
    为true
    。从
    x
    读取的任何其他尝试都会导致数据竞争,因此是未定义的行为

  • 无论何时访问该标志,都必须告诉系统由线程设置写入的值,该标志应该对看到该设置值的后续操作可见。这需要一个正确的内存顺序,它必须至少是
    内存\u顺序\u获取

    从技术上讲,从标志本身读取不必进行获取。从标志中读取正确的值后,可以执行获取操作。但是,在读取
    x
    之前,需要执行一个获取等效操作

  • writing语句必须使用释放内存顺序设置标志,该顺序必须至少与
    内存\u顺序\u释放
    一样强大

  • 因为(1)和(2)取决于if条件,CPU必须等待ready_标志

    这种推理有两个缺陷:

  • 分支预测+推测执行在真实的CPU中是真实的。控件依赖项的行为不同于数据依赖项推测性执行会破坏控制依赖关系。

    大多数(但不是全部)实际CPU,数据依赖性像C++ >代码>内存yOrthOrthOuto.<代码>。一个典型的用例是加载指针,然后取消引用它。这在C++非常弱的内存模型中仍然是不安全的,但编译到asm时会发生这种情况,asm适用于除DEC Alpha之外的大多数ISA。Alpha(在某些硬件上)甚至可以在取消引用刚加载的指针时违反因果关系并加载过时的值,即使存储顺序正确

  • 编译器可以破坏控制甚至数据依赖关系。C++源逻辑并不总是直接转换为ASM。<强>在此编译器可以发出这样的ASM:

     tmp = load(x);         // compile time reordering before the relaxed load
     if (load(ready_flag)
        actually use tmp;
    
    <> P> C++中的数据族UB读取代码> X/COD>虽然它可能仍在编写,但是对于大多数特定的ISAS来说,这没有问题。您只需要避免实际使用任何可能是虚假的加载结果

    对于大多数ISA来说,这可能不是一个有用的优化,但没有什么可以排除它。通过更早地加载来隐藏顺序管道上的加载延迟实际上有时可能很有用(如果它不是由另一个线程编写的,编译器可能会猜测这不会发生,因为没有获取加载)

  • 到目前为止,您的最佳选择是使用
    ready\u flag.load(mo\u acquire)


    另一个问题是,您已经注释掉了在
    if()
    之后读取
    x
    的代码,即使加载未看到数据就绪,该代码也将运行。正如@Nicol在回答中所解释的,这意味着数据竞争是可能的,因为您可能已经准备好了
    // thread-2
    x = 100;
    ready_flag.store(true, std::memory_order_release);
    
     tmp = load(x);         // compile time reordering before the relaxed load
     if (load(ready_flag)
        actually use tmp;
    
     if (ready_flag.load(mo_relaxed))
         atomic_thread_fence(mo_acquire);
         int tmp = x;   // now this is safe
     }
     // atomic_thread_fence(mo_acquire);  // still wouldn't make it safe to read x
     // because this code runs even after ready_flag == false