在此参考代码的[1]中有两个关于C ++ 11静态初始化的问题(下面)(这是一个经过完整测试的c ++ 11示例程序)。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
struct Foo {
/* complex member variables. */
};
void DoSomething(Foo *foo) {
// Complex, but signal safe, use of foo.
}
Foo InitFoo() {
Foo foo;
/* complex, but signal safe, initialization of foo */
return foo;
}
Foo* GetFoo() {
static Foo foo = InitFoo(); // [1]
return &foo;
}
void Handler(int sig) {
DoSomething(GetFoo());
}
int main() {
// [2]
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = Handler;
sigaction(SIGINT, &act, nullptr);
for (;;) {
sleep(1);
DoSomething(GetFoo());
}
}
问题1:这是否保证安全(无死锁等)? C ++ 11静态初始化涉及锁。如果在main中第一次调用GetFoo()之前/之后/期间传递信号怎么办?
问题2:如果在安装信号处理程序之前在[2]处插入对GetFoo()的调用,这是否保证安全?
我在最近的GNU / Linux上假设使用C ++ 11(g ++或clang),尽管各种Unices的答案也很有趣。 (剧透:我认为答案是1:否和2:是,但我不知道如何证明这一点。)
答案 0 :(得分:1)
static Foo foo = InitFoo();
的初始化方式必须首先说明,然后再进入信号。
它需要dynamic initialization,在GetFoo()
第一次被调用时它将被初始化,因为您在InitFoo()
中提到的“复杂初始化”在编译时无法完成:
具有静态存储的块范围变量的动态初始化 持续时间或线程存储持续时间是第一次执行 控制通过其声明;这样的变量被认为 初始化完成后初始化。如果 初始化通过抛出异常退出,初始化是 未完成,因此下次进入控件时将再次尝试 声明。 如果控件在同时输入声明 变量正在初始化,并发执行应等待 完成初始化。 85 如果在初始化变量时控件递归地重新输入声明,则 行为是不确定的。
85 该实现不得在初始化程序的执行过程中引入任何死锁。死锁可能仍然是由程序逻辑引起的。该实现只需避免由于自身的同步操作而导致的死锁。
在建立了这个之后,我们可以去提问。
问题1:这是否保证安全(无死锁等)? C ++ 11静态初始化涉及锁。如果在main中第一次调用GetFoo()之前/之后/期间传递信号怎么办?
不,不能保证。考虑一下何时从GetFoo()
循环内部调用for
的时间:
GetFoo() -> a lock is taken to initialize 'foo'-> a signal arrives [control goes to signal handling function] -> blocked here for signal handling to complete
--> Handler() -> DoSomething(GetFoo()) -> GetFoo() -> waits here because the lock is unavailable.
(由于'foo'的初始化尚未完成,信号处理程序必须在这里等待-请参阅上面的引言)。
因此在这种情况下(即使没有任何线程)也会由于线程自身被阻塞而发生死锁。
问题2:如果在安装信号处理程序之前在[2]处插入对GetFoo()的调用,这是否保证安全?
在这种情况下,根本没有为SIGINT建立信号处理程序。因此,如果SIGINT
到达,程序将退出。 SIGINT的 default 处置是终止该过程。 GetFoo()
的初始化是否正在进行都无关紧要。这样很好。
情况(1)的基本问题是信号处理程序Handler
不是async-signal-safe,因为它调用的GetFoo()
不是异步信号安全的。
重新。更新了有关静态初始化的可能实现的问题:
C ++ 11标准仅保证foo
的初始化是以线程安全的方式完成的(请参见上面的粗体引用)。但是处理信号不是“并发执行”。它更像是“递归重新进入”,因为即使在单线程程序中也可能发生这种情况-因此它是不确定的。即使像在您的第二种方法中实现了避免死锁的静态初始化一样,也是如此。
用另一种方式:如果像您的第一个方法一样实现静态初始化,是否违反了标准?答案是不。因此,您不能依靠以异步信号安全的方式实现静态初始化。
鉴于您确保“ ...只要代码处理程序在信号处理程序运行之前至少已完全运行一次”。那么您可以引入另一种检查,以确保GetFoo()
都是异步信号安全的,而无论如何实现静态初始化:
std::atomic<bool> foo_done = false;
static_assert( std::atomic<bool>::is_lock_free );
Foo* GetFoo() {
if (!foo_done) {
static Foo foo = InitFoo(); // [1]
foo_done = true;
return &foo;
}
}