多线程程序中的std :: atomic <int> memory_order_relaxed VS volatile sig_atomic_t

时间:2019-06-14 13:14:19

标签: c++ multithreading volatile memory-model stdatomic

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

例如在这里:

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

它是正确的代码吗? 有条件可能不起作用吗?

注意:这是一个过于简单的示例,即我不是在寻找给定代码段的更好解决方案。我只是想了解在根据C ++标准的多线程程序中,volatile sig_atomic_t会带来什么样的行为。或者,在这种情况下,请理解为什么行为未定义。

我发现以下语句here

  

库类型sig_atomic_t不提供线程间同步或内存排序,仅提供原子性。

如果我将其与此定义here进行比较:

  

memory_order_relaxed:轻松的操作:对其他读取或写入没有同步或排序约束,仅保证此操作的原子性

不一样吗? atomicity 在这里到底是什么意思? volatile在这里有用吗? “不提供同步或内存排序”与“不提供同步或内存排序约束”有什么区别?

1 个答案:

答案 0 :(得分:8)

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

std::sig_atomic_tstd::atomic<T>属于不同的联盟。在便携式代码中,一个不能由另一个替换,反之亦然。

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

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

sig_atomic_t的用途是什么?

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

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

例如:

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
    {
        ...
    }
}

尽管此代码是单线程的,但是do_work可以被触发sig_handler并自动改变quit值的信号异步中断。 如果没有volatile,编译器可能会从quit的while循环中“提升”负载,从而使do_work无法观察到信号引起的quit的变化。

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

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

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

想象一下上面的代码,其中quit标志是一个std::atomic<int>碰巧不是非锁定的。当do_work()加载该值时, 在获取锁定之后但在释放它之前,该信号会被信号中断。 信号触发sig_handler(),现在它想要通过采取相同的锁定来将值存储到quit,哎呀,已经被do_work获取了。这是未定义的行为,可能会导致死锁。
std::sig_atomic_t不存在此问题,因为它不使用锁定。所需要的只是一种在CPU级别和许多平台上都是不可分割的类型,它可以很简单:

typedef int sig_atomic_t;

最重要的是,在多线程环境中,将volatile std::sig_atomic_t用作单线程中的信号处理程序,并将std::atomic<T>用作无数据争用类型。