信号不安全函数在子线程中出问题的根本原因是其内部依赖全局状态(如堆管理结构、FILE缓冲区),且未设计为可被信号处理函数并发调用;一旦信号中断线程执行并进入同样调用该函数的handler,就会破坏一致性,导致abort或死锁。

子线程里调用信号不安全函数(比如 malloc、printf、std::string::assign、std::queue::push)是 C++ 多线程中一类隐蔽但致命的 Bug 来源——它不一定会 crash,但一旦触发,往往直接 abort 或死锁,且极难复现。
为什么 signal-unsafe 函数在子线程里会出问题?
根本原因不是“线程不能调用”,而是这些函数内部可能依赖全局状态(如 malloc 的堆管理结构、stdio 的 FILE 缓冲区),而它们的实现并未考虑被信号处理函数 *同时* 调用的场景。当一个信号(如 SIGUSR1、SIGALRM)在子线程执行到 printf 中间时被投递,且信号处理函数也调用了 printf,就可能破坏堆或缓冲区一致性。
更麻烦的是:C++ 标准库绝大多数接口(包括 std::string、std::vector、std::queue)都属于 signal-unsafe —— 它们不保证在信号上下文中可重入,也不承诺内部无锁或无全局状态。
- 信号处理函数必须只调用 async-signal-safe 函数(POSIX 明确列出约 120 个,如
write、sigprocmask、raise) - 子线程本身不是信号处理上下文,但若该线程被设为信号接收者(如用
pthread_sigmask解除了阻塞),它就可能在任意位置被中断并进入信号处理函数 - 常见误判:“我没写 signal handler,所以没问题”——错。系统库(如 glibc 的定时器、profiler、甚至某些调试器)可能注册了信号 handler,而你的子线程正在调用
std::string::operator+=,此时就构成竞态
哪些 C++ 操作在子线程里特别危险?
以下操作在子线程中看似正常,但一旦线程被信号中断,或与信号 handler 共享资源,就极易崩溃:
立即学习“C++免费学习笔记(深入)”;
-
std::cout/printf:内部用锁保护 stdout,但锁本身不是 async-signal-safe;信号 handler 若也写 stdout,会死锁或破坏 FILE 结构 -
new/malloc:堆管理器(如 ptmalloc)的 arena 锁不是信号安全的;并发 malloc 可能导致 double-free 或 segfault -
std::string构造/赋值/拼接:涉及内存分配 + 内部引用计数(旧实现)或小字符串优化(SSO)切换,非原子 -
std::queue::push/std::vector::push_back:可能触发内存重分配,涉及 malloc + memcpy,完全不可重入 -
std::mutex::lock:虽然 mutex 本身是线程安全的,但其底层 futex 系统调用在信号中断时可能留下未清理状态(尤其在老内核上)
如何检测和规避这类问题?
没有银弹,但可通过组合策略大幅降低风险:
- 禁止在信号 handler 中调用任何 C++ 标准库函数;只用
write、siglongjmp、atomic_store等 async-signal-safe 接口 - 子线程中避免在关键路径(如循环体、回调入口)使用动态分配或格式化 I/O;改用预分配缓冲区 +
snprintf+write - 用
pthread_sigmask在子线程启动时屏蔽所有信号(除SIGKILL、SIGSTOP),由专门的信号接收线程统一处理 - 启用
-fsanitize=address,undefined和-D_GLIBCXX_DEBUG编译,部分内存破坏会在 debug 模式下提前暴露 - 注意第三方库:如 libcurl、protobuf 的某些 API 也是 signal-unsafe,需查其文档确认
最常被忽略的一点:即使你没显式安装 signal handler,glibc 的 timer_create、setitimer、甚至 std::this_thread::sleep_for 在某些实现中都可能间接触发信号;只要子线程在做任何非 trivial 的 C++ 对象操作,就得默认它处在潜在的信号干扰路径上。


















