std::atomic<int> 多线程程序中的内存_order_relaxed VS 易失性sig_atomic_t

问题描述 投票:0回答:1

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
获得什么样的行为。或者,如果是这种情况,请理解为什么行为是未定义的。

我在这里找到了以下声明:

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

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

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

是不是不太一样? 原子性在这里到底意味着什么?

volatile
在这里有什么用处吗? “不提供同步或内存排序”和“无同步或排序约束”之间有什么区别?

c++ multithreading volatile memory-model stdatomic
1个回答
10
投票

您正在使用

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

std::sig_atomic_t
std::atomic<T>
处于不同的联盟。在portable代码中,一个不能被另一个替换,反之亦然。

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

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
使用锁可通过成员函数
is_lock_free()
或类常量
is_always_lock_free
(C++17) 获得。

在信号处理程序中使用此类型的问题在于,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>
作为无数据竞争类型。

© www.soinside.com 2019 - 2024. All rights reserved.