尝试从“C++ Concurrency in Action”(第 133 页)一书中用原子理解代码示例

Posted

技术标签:

【中文标题】尝试从“C++ Concurrency in Action”(第 133 页)一书中用原子理解代码示例【英文标题】:Trying to understand code example with atomics from book "C++ Concurrency in Action" (page 133) 【发布时间】:2020-02-01 14:12:22 【问题描述】:

我目前正在尝试从“C++ Concurrency in Action”一书中理解以下代码示例:

#include <stdio.h>

#include <atomic>
#include <thread>

#undef NDEBUG // for release builds
#include <assert.h>

std::atomic<bool> x, y;
std::atomic<int> z;

void write_x()

    x.store(true, std::memory_order_release);


void write_y()

    y.store(true, std::memory_order_release);


void read_x_then_y()

    while (!x.load(std::memory_order_acquire))
        ;
    if (y.load(std::memory_order_acquire))
        ++z;


void read_y_then_x()

    while (!y.load(std::memory_order_acquire))
        ;
    if (x.load(std::memory_order_acquire))
        ++z;


int main(int argc, char *argv[])

    for (;;)
    
        x = false;
        y = false;
        z = 0;

        std::thread a(write_x);
        std::thread b(write_y);
        std::thread c(read_x_then_y);
        std::thread d(read_y_then_x);

        a.join();
        b.join();
        c.join();
        d.join();

        assert(z.load() != 0);
    

    return 0;

我不太明白断言是如何触发的,正如书中所声称的那样。四个线程的代码行如何必须并排排列,以使 z 等于 0。原则上,只有在自旋锁之后可以重新排列线条时才会发生这种情况,对吧?但据我了解,Acquire 保证 Acquire 之后的 Loads 和 Stores 不能在 Acquire 之前重新排序。并且 Acquire 之前的 Load 不能在 Acquire 之后重新排序。

提前感谢您的澄清。

编辑

我认为如果有人从技术角度(缓存、围栏、重新排序、刷新、无效等)并使用 z=0 的案例研究来解释整个事情会更容易理解。

解释它的示例,但我需要它用于 z=0(技术上):

Thread 1  Thread 2    Thread 3        Thread 4

                      while (!x);     while (!y);
x=1                   while (!x);     while (!y);
                      if (y) // y=0   while (!y);
          y=1                         while (!y);
                                      while (!y);
                                      if (x) // x=1
                                      z++

结果:z=1

我也不太明白的是:为什么必须为商店指定释放约束?为什么这里不够放松?原则上,在必须刷新或必须通过栅栏防止其重新排序的存储之上没有指定进一步的操作(加载、存储)。 write_x 不就相当于这个吗?

// Store / loads which may not cross fence boundary.
// No store / loads here, so why needs to be a fence here?
std::atomic_thread_fence(std::memory_order_release);
x.store(true, std::memory_order_relaxed);

我已经在我的 x86-64 上运行了很长时间(无休止的 for 循环)上面的示例,但还没有遇到断言。这是因为英特尔强大的内存模型吗?

编辑 2

隔了半天重温这个话题,发现***线程如下:Will two atomic writes to different locations in different threads always be seen in the same order by other threads?。我想我通过这个找到了技术层面的解释。即,该结果可以如下产生。前提条件:具有 SMT(超线程)功能的弱序 CPU。似乎一个逻辑处理器可以从运行在同一内核上的另一个逻辑处理器的共享存储缓冲区中提取数据(= 存储转发)。从示意图上看,它是这样的:


+--------------------------------------------------------+
| Core 0                                                 |
+--------------------------------------------------------+
| Logical Core #0                                        |
+--------------------------------------------------------+
| x.store(true, std::memory_order_release);              | <- Place x into StoreBuffer first before committing to L1D
+--------------------------------------------------------+
| Logical Core #1                                        |
+--------------------------------------------------------+
| while (!x.load(std::memory_order_acquire))             | <- Read new x from StoreBuffer which is not visible for other Cores yet (x = true)
|   ;                                                    |
| if (y.load(std::memory_order_acquire))                 | <- new y not visible yet, still in StoreBuffer of other Core (y = false)
|   ++z;                                                 |
|                                                        |
+--------------------------------------------------------+

Same behavior for other Core:

+--------------------------------------------------------+
| Core 1                                                 |
+--------------------------------------------------------+
| Logical Core #0                                        |
+--------------------------------------------------------+
| y.store(true, std::memory_order_release);              | <- Place y into StoreBuffer first before committing to L1D
+--------------------------------------------------------+
| Logical Core #1                                        |
+--------------------------------------------------------+
| while (!y.load(std::memory_order_acquire))             | <- Read new y from StoreBuffer which is not visible for other Cores yet (y = true)
|   ;                                                    |
| if (x.load(std::memory_order_acquire))                 | <- new x not visible yet, still in StoreBuffer of other Core (x = false)
|   ++z;                                                 |
|                                                        |
+--------------------------------------------------------+

我的假设是否正确,谁能证实这一点?提前谢谢你。

【问题讨论】:

atomic x,y,z; 使 no 有意义。 std::atomic 需要一个类型模板参数。无论如何,请始终发布minimal reproducible example。否则其他人无法测试您的代码。 "// 伪 C++ 中的简化示例" - 不,不要那样做。改为发布minimal reproducible example。 @JesperJuhl OP 确实提到它是简化的伪 C++。我认为它们的意思很清楚。 @OP:本书的下一页以漂亮的图表和描述解释了发生的事情。你到底有什么不清楚的地方? 请不要发布涉及每个人都可能没有的书籍的问题。 所有相关信息都需要问题中! 【参考方案1】:

问题是 x 和 y 的两个 store 没有相互排序,因此它们不能用于同步可见性:

atomic x,y,z;
void write_x() 
  x.store(true, release);

void write_y() 
  y.store(true, release);


void read_x_then_y() 
  while (!x.load(acquire))
    ;
  // has seen x==true, but there is no guarantee that y.load() will return true
  if (y.load(acquire))
    ++z;


void read_y_then_x() 
  while (!y.load(acquire))
    ;
  // has seen y==true, but there is no guarantee that x.load() will return true
  if (x.load(acquire))
    ++z;


void main() 
  x=false;
  y=false;
  z=0;
  thread a(write_x);
  thread b(write_y);
  thread c(read_x_then_y);
  thread d(read_y_then_x);
  join(a,b,c,d);
  assert(z.load()!=0);

因此可能会发生两个线程在 while 循环之后读取 false,因此 z 永远不会递增。

关键是商店操作使用memory_order_release,因此没有单一的总订单,这也是为什么并排排列线无助于解释这种行为的原因。

【讨论】:

【参考方案2】:

Acquire/release 仅在执行获取操作的线程中执行获取操作之前的存储排序。

在获取操作之前没有存储,因此相对于宽松的情况,实际上没有施加额外的排序。

在宽松的情况下唯一强加的顺序是原子变量单独具有所有线程都同意的总修改顺序。但是两个线程可能会在您的示例中不同原子上的两个商店中的哪一个首先发生的问题上存在分歧。

【讨论】:

以上是关于尝试从“C++ Concurrency in Action”(第 133 页)一书中用原子理解代码示例的主要内容,如果未能解决你的问题,请参考以下文章

尝试从智能按钮创建订阅时出错

尝试从 docker 容器访问时,数据存储返回 503

为啥我的 Flask 尝试从不同的目录导入?

尝试使用环境变量从 HostingViewController 呈现 SwiftUI

尝试从数组创建 DAO 记录集

尝试从网络访问 XAMPP 时出错 [关闭]