多线程程序中std::atomic<int>memory_order_relaxedVSvolatilesig_atomic_t

std::atomic<int> memory_order_relaxed VS volatile sig_atomic_t in a multithreaded program

volatile sig_atomic_t 是否提供任何内存顺序保证?例如。如果我只需要 load/store 一个整数可以使用吗?

例如这里:

volatile sig_atomic_t x = 0;
...
void f() {
  std::thread t([&] {x = 1;});
  while(x != 1) {/*waiting...*/}
  //done!
}

代码正确吗? 在某些情况下它可能不起作用?

注意:这是一个过度简化的示例,即我不是在为给定的代码段寻找更好的解决方案。我只想了解根据 C++ 标准,在多线程程序中我可以期望 volatile sig_atomic_t 有什么样的行为。或者,如果是这种情况,请理解行为未定义的原因。

我发现了以下语句 here:

The library type sig_atomic_t does not provide inter-thread synchronization or memory ordering, only atomicity.

如果我将它与这个定义进行比较 here:

memory_order_relaxed: Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation's atomicity is guaranteed

不一样吗? atomicity 在这里到底是什么意思? volatile 在这里有什么用吗? "does not provide synchronization or memory ordering" 和 "no synchronization or ordering constraints" 有什么区别?

您正在使用一个 sig_atomic_t 类型的对象,该对象由两个线程访问(一个修改)。
根据 C++11 内存模型,这是未定义的行为,简单的解决方案是使用 std::atomic<T>

std::sig_atomic_tstd::atomic<T> 属于不同的级别。在 portable 代码中,一个不能被另一个替换,反之亦然。

两者唯一共享的 属性 是原子性(不可分割的操作)。这意味着对这些类型的对象的操作没有(可观察到的)中间状态,但这就是相似之处。

sig_atomic_t 没有 inter-thread 属性。事实上,如果这种类型的对象被多个线程访问(修改)(如在您的示例代码中),它在技术上是未定义的行为(数据竞争); 因此,inter-thread 内存排序属性未定义。

sig_atomic_t有什么用?

这种类型的对象可以在信号处理程序中使用,但前提是它被声明 volatile。原子性和 volatile 保证了两件事:

  • 原子性:信号处理程序可以将值异步存储到对象,任何读取同一变量(在同一线程中)的人只能观察之前或之后的值。
  • volatile:编译器不能 'optimized away' 存储,因此在信号中断执行的位置(或之后)可见(在同一线程中)。

例如:

volatile sig_atomic_t quit {0};

void sig_handler(int signo)  // called upon arrival of a signal
{
    quit = 1;  // store value
}


void do_work()
{
    while (!quit)  // load value
    {
        ...
    }
}

虽然此代码是 single-threaded,但 do_work 可以被触发 sig_handler 的信号异步中断并自动更改 quit 的值。 如果没有 volatile,编译器可能会 'hoist' 从 quit 加载 while 循环,使得 do_work 无法观察到由 quit 引起的变化一个信号。

为什么不能用std::atomic<T>代替std::sig_atomic_t

一般来说,std::atomic<T> 模板是一种不同的类型,因为它被设计为由多个线程并发访问并提供 inter-thread 顺序保证。 原子性并不总是在 CPU 级别可用(尤其是对于较大的类型 T),因此实现可能使用内部锁来模拟原子行为。 std::atomic<T> 是否对特定类型 T 使用锁可通过成员函数 is_lock_free() 或 class 常量 is_always_lock_free (C++17) 获得。

在信号处理程序中使用此类型的问题是 C++ 标准不保证 std::atomic<T> 对于任何类型 T 都是 lock-free。只有 std::atomic_flag 有这种保证,但那是另一种类型。

想象上面的代码,其中 quit 标志是 std::atomic<int> 而恰好不是 lock-free。当 do_work() 加载值时,有可能, 它在获取锁后但在释放锁之前被信号中断。 信号触发了 sig_handler(),它现在想要通过获取相同的锁来将值存储到 quit,这已经被 do_work 获取了,哎呀。这是未定义的行为,可能会导致 dead-lock.
std::sig_atomic_t 没有那个问题,因为它不使用锁定。所需要的只是一种在 CPU 级别不可分割的类型,并且在许多平台上,它可以像这样简单:

typedef int sig_atomic_t;

底线是,在单线程中使用 volatile std::sig_atomic_t 作为信号处理程序,在多线程环境中使用 std::atomic<T> 作为 data-race-free 类型。