#include <unistd.h>
#include <csignal>
#include <exception>
#include <functional>
#include <iostream>
std::function<void(int)> g_signalHandler;
void signalWrapper(const int a) { g_signalHandler(a); }
int main() {
bool abort = false;
g_signalHandler = [&abort](const int) {
std::cout << "Abort" << std::endl;
abort = true;
};
struct ::sigaction signalAction = {};
signalAction.sa_handler = signalWrapper;
if (
::sigaction(SIGHUP, &signalAction, nullptr) != 0 ||
::sigaction(SIGINT, &signalAction, nullptr) != 0 ||
::sigaction(SIGTERM, &signalAction, nullptr) != 0
)
throw std::exception();
// Is it guaranteed that the new value of abort will be seen here without
// the need of std::atomic<bool> or volatile bool?
while (!abort)
::sleep(1);
return 0;
}
假设一个单线程程序、C++17 和 Linux
据我所知,使用普通的bool
就足够了,while (!abort)
并且std::atomic<bool>
/volatile bool
不是必要的。我想确认这始终是正确的,并且我不必担心编译器优化读取值
不,这是错误的,并且会引起数据竞争,而这始终是未定义的行为。
为了说明这种未定义行为如何合理地表现出来:编译器可能会认为
abort
是非原子的,并且::sleep
并不意味着任何同步。因此,它可以得出结论,abort
如果最初是false
,则永远不会改变,因为这会导致具有未定义行为的数据争用,并且要么完全摆脱循环检查,要么将abort
其保存在寄存器中而不从内存中重新加载。无论哪种情况,您的循环都不会终止。volatile bool
也是错误的。volatile
根据信号处理程序标准的要求,这是不够的。volatile
确实保证代码中的每个读取和写入实际上都会转换为加载和存储,但是volatile
不保证这些加载/存储的原子性。(但是,特定的编译器/平台组合可能会做出这样的保证,因此volatile
可用于实现原子操作。)在信号处理程序中,唯一可以保证能够以定义的行为访问的非局部变量是(非线程局部)
volatile std::atomic_sig_t
(并通过 glvalue 访问volatile
);仅当在信号处理程序和运行信号处理程序的线程之间共享(无需其他同步)时,其他线程才不共享std::atomic_flag
保证无锁;哪些专业化std::atomic
是无锁的,这取决于平台,可以使用is_lock_free
和is_always_lock_free
成员进行检查std::atomic
此外,默认情况下,库函数在信号处理程序中使用也不安全。C/C++/POSIX 标准中只有特定的例外。具体来说,
std::function
流 IOstd::cout <<
在信号处理程序中不安全,也会导致未定义的行为。请参阅https://en.cppreference.com/w/cpp/utility/program/signal了解 C++ 标准对信号处理程序的要求,以便它们根据标准具有明确定义的行为。
请参阅https://pubs.opengroup.org/onlinepubs/9799919799/functions/V2_chap02.html#tag_16_04_03以获取POSIX 环境中异步信号安全函数的列表。