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
在这里有用吗? “不提供同步或内存排序”与“不提供同步或内存排序约束”有什么区别?
答案 0 :(得分:8)
您正在使用sig_atomic_t
类型的对象,该对象由两个线程(一个修改)访问。
根据C ++ 11内存模型,这是未定义的行为,简单的解决方案是使用std::atomic<T>
std::sig_atomic_t
和std::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>
用作无数据争用类型。