您正在使用一个被两个线程(一个进行修改)访问的
sig_atomic_t
类型的对象。
根据 C++11 内存模型,这是未定义的行为,简单的解决方法是使用
std::atomic<T>
。
std::sig_atomic_t
和 std::atomic<T>
不同级别。在可移植代码中,它们不能互换使用。
它们唯一共享的属性是原子性(不可分割的操作)。这意味着对这些类型的对象的操作没有(可观察到的)中间状态,但仅限于此。
sig_atomic_t
没有跨线程的属性。实际上,如果多个线程(如您的示例代码中)访问(修改)此类型的对象,则技术上是未定义行为(数据竞争); 因此,未定义交叉线程内存排序属性。
什么是 sig_atomic_t
用于?
此类型的对象可以在信号处理程序中使用,但只有在声明为 volatile
时才能使用。原子性和 volatile
保证两件事:
- 原子性:信号处理程序可以异步地将值存储到对象中,并且在同一线程中读取同一变量的任何人只能观察到之前或之后的值。
- volatile:存储不能被编译器“优化”,因此在信号中断执行后(或之后)点可见(在同一线程中)。
例如:
volatile sig_atomic_t quit {0};
void sig_handler(int signo)
{
quit = 1;
}
void do_work()
{
while (!quit)
{
...
}
}
尽管此代码是单线程的,但是
do_work
可以被异步中断,触发
sig_handler
并原子性地更改
quit
的值。
如果没有使用
volatile
,编译器可能会将从 while 循环中的
quit
加载“提升”出来,这样
do_work
就无法观察到由信号引起的对
quit
的更改。
为什么不能用 std::atomic<T>
替换 std::sig_atomic_t
?
一般来说,
std::atomic<T>
模板是一种不同的类型,因为它被设计为可以同时被多个线程访问并提供线程间排序保证。原子性并不总是在 CPU 级别上可用(特别是对于较大的类型
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()
,现在它想通过获取与
do_work
已经获取的相同的锁来存储一个值到
quit
中,糟糕了。这是未定义的行为,可能会导致死锁。
std::sig_atomic_t
没有这个问题,因为它不使用锁定。所需的只是一种在 CPU 级别上不可分割的类型,在许多平台上,它可以非常简单:
typedef int sig_atomic_t;
总的来说,在单线程环境下使用 volatile std::sig_atomic_t
处理信号处理程序,在多线程环境下使用 std::atomic<T>
作为无数据竞争类型。
std::atomic_signal_fence
的文档,我认为与 'signal' 相关的任何顺序保证都不存在。 - Mgetz