C++:std::atomic 仍然需要 __sync_synchronize() 吗?

Swi*_*tch 2 c++ linux atomic

我遇到了一种罕见但反复发生的竞争状况。该程序有两个线程并使用 std::atomic。我将简化代码的关键部分,如下所示:

std::atomic<uint64_t> b;  // flag, initialized to 0
uint64_t data[100];  // shared data, initialized to 0
Run Code Online (Sandbox Code Playgroud)

线程 1(发布):

// set various shared variables here, for example
data[5] = 10;

uint64_t a = b.exchange(1);  // signal to thread 2 that data is ready
Run Code Online (Sandbox Code Playgroud)

线程2(接收):

if (b.load() != 0) {  // signal that data is ready
  // read various shared variables here, for example:
  uint64_t x = data[5];
  // race condition sometimes (x sometimes not consistent)
}
Run Code Online (Sandbox Code Playgroud)

奇怪的是,当我向每个线程添加 __sync_synchronize() 时,竞争条件就会消失。我已经在两个不同的服务器上看到过这种情况发生。

即当我将代码更改为如下所示时,问题就消失了:

线程 1(发布):

// set various shared variables here, for example
data[5] = 10;

__sync_synchronize();
uint64_t a = b.exchange(1);  // signal to thread 2 that data is ready
Run Code Online (Sandbox Code Playgroud)

线程2(接收):

if (b.load() != 0) {  // signal that data is ready
  __sync_synchronize();
  // read various shared variables here, for example:
  uint64_t x = data[5];
}
Run Code Online (Sandbox Code Playgroud)

为什么需要 __sync_synchronize() ?这似乎是多余的,因为我认为交换和加载都确保了逻辑的正确顺序。

架构是 x86_64 处理器、linux、g++ 4.6.2

Mat*_*son 6

虽然不可能从简化的代码中说出实际应用程序中实际发生的情况,但有__sync_synchronize帮助的事实以及该函数是内存屏障的事实告诉我,您正在一个线程中编写其他线程正在编写的内容阅读,以一种非原子的方式。

一个例子:

thread_1:

    object *p = new object;
    p->x = 1;
    b.exchange(p);   /* give pointer p to other thread */

thread_2:

    object *p = b.load();
    if (p->x == 1) do_stuff();
    else error("Huh?");
Run Code Online (Sandbox Code Playgroud)

这很可能会触发线程 2 中的错误路径,因为当线程 2 读取新的指针值 p 时,写入p->x实际上尚未完成。

在本例中,在 thread_1 代码中添加内存屏障应该可以解决此问题。请注意,对于这种情况,thread_2 中的内存屏障不会做任何事情 - 它可能会改变时间并似乎解决问题,但这不是正确的事情。如果您正在读取/写入两个线程之间共享的内存,则两侧可能仍然需要内存屏障。

我知道这可能不完全是您的代码正在做的事情,但概念是相同的 -__sync_synchronize确保在该函数调用之前所有指令的内存读取和内存写入已完成[这不是真正的函数调用,它将内联一条等待任何挂起的内存操作完成的指令]。

值得注意的是,对std::atomic原子对象的操作仅影响存储在原子对象中的实际数据。不读取/写入其他数据。

有时您还需要“编译器屏障”来避免编译器将内容从操作的一侧移动到另一侧:

  std::atomic<bool> flag(false);
  value = 42;
  flag.store(true);

  ....
Run Code Online (Sandbox Code Playgroud)

另一个线程:

  while(!flag.load());
  print(value); 
Run Code Online (Sandbox Code Playgroud)

现在,编译器有可能生成第一种形式:

  flag.store(true);
  value = 42;
Run Code Online (Sandbox Code Playgroud)

现在,这不太好,不是吗?std::atomic 保证是“编译器屏障”,但在其他情况下,编译器很可能以类似的方式对内容进行洗牌。