信号处理函数中调用 std::mutex::lock() 是未定义行为,几乎必然导致死锁,因其非异步信号安全,可能在持有锁时被中断而重复加锁或引发内核资源争用。

信号处理函数中调用 std::mutex::lock() 是未定义行为,几乎必然导致死锁——因为信号可能在任意时刻中断线程,包括正在持有锁的临界区内部。
为什么 signal handler 里 lock 会死锁
信号是异步的,std::mutex 的实现(如 futex)不是异步信号安全的。当信号到达时:
- 若当前线程正持有一个
std::mutex并在临界区内执行(比如刚调用完lock()、还没unlock()),信号 handler 再次对同一 mutex 调用lock(),就会立即死锁(同一线程重复 lockstd::mutex); - 即使锁对象不同,也可能因内核级资源争用(如 futex 等待队列操作)触发未定义行为,表现为挂起、崩溃或静默失败;
-
std::lock_guard、std::unique_lock等 RAII 类型的构造函数内部调用lock(),同样不可在 signal handler 中使用。
signal handler 中唯一安全的同步手段
POSIX 明确规定,只有少数函数是异步信号安全的(async-signal-safe)。与锁相关的仅有:
-
sig_atomic_t类型的全局变量读写(必须是单一机器指令可完成的,如int或sig_atomic_t,且不能是位域或非对齐访问); -
write()(仅限于文件描述符为标准错误或日志 fd,且数据量小); -
raise()、signal()(仅用于重置 handler 自身); - 绝对不要用
std::mutex、std::condition_variable、malloc、printf、std::cout、任何 STL 容器操作。
典型安全模式:
volatile sig_atomic_t g_shutdown_requested = 0;
void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
g_shutdown_requested = 1; // 安全:单字节/字写入
}
}
如何安全地响应信号并协同多线程退出
主线程注册 signal handler,只做最低限度标记;其他线程轮询或通过管道/事件fd感知变化,再在常规上下文中执行加锁和清理:
- 用
signalfd()(Linux)或self-pipe trick把信号转为文件描述符事件,接入 epoll/poll 循环,完全避开 signal handler; - 主线程设置
sig_atomic_t标志后,用pthread_kill()向工作线程发SIGUSR1(需提前用pthread_sigmask()屏蔽该信号),再由各线程在安全点检查标志; - 避免在 signal handler 中调用
std::condition_variable::notify_one()—— 它不是 async-signal-safe;改用eventfd_write()或管道write()触发等待线程; - 若必须通知某线程停止并等待其释放锁,应在该线程自己的循环中检测退出标志,主动退出临界区后 join,而非从外部强制干预。
调试这类死锁的关键线索
现象通常是程序收到信号后卡死,但无 core dump、无日志输出。排查时重点关注:
- 用
gdb attach <pid></pid>后执行thread apply all bt,看是否有线程停在futex_wait或pthread_mutex_lock内部,同时 signal handler 帧出现在栈顶; - 检查所有 signal handler 实现,确认是否直接或间接(如通过日志宏、封装函数)调用了任何非 async-signal-safe 函数;
- 用
strace -p <pid></pid>观察是否出现阻塞在futex(... FUTEX_WAIT ...)的系统调用; - 启用
-fsanitize=thread编译,它能在部分场景下捕获 signal handler 中的非法同步操作(尽管覆盖率有限)。
真正棘手的不是“怎么加锁”,而是“根本不能在这里加锁”——信号上下文的约束比多线程逻辑更硬性,绕不开,也骗不过。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











