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
在这里有什么用处吗? “不提供同步或内存排序”和“无同步或排序约束”之间有什么区别?
您正在使用
sig_atomic_t
类型的对象,该对象由两个线程访问(其中一个线程进行修改)。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>
作为无数据竞争类型。