asyncio信号处理需用create_task而非await,SIGPIPE须显式忽略,异步资源close后必须wait_closed,信号注册仅主线程有效,退出路径须覆盖所有资源类型。

asyncio 里不能在 signal handler 里 await 任何协程
这是最常踩的坑:你写了 handle_exit 函数,里面直接 await session.close() 或 await pool.close(),Python 会立刻报 RuntimeError: await outside async function —— 因为信号处理器是同步函数,事件循环此时可能还没启动、已停止,或正被阻塞,根本无法调度协程。
正确做法是把清理逻辑转成「事件循环可调度任务」:
- 用
loop.create_task()包一层,让清理变成待执行 task - 在 handler 里只调用
loop.stop(),不 await 任何东西 - 主流程退出前,用
asyncio.gather(..., return_exceptions=True)等待所有清理 task 完成
SIGPIPE 必须显式忽略,否则网络写失败直接进程崩溃
Linux 下对已关闭连接调用 send() 或 write(),内核默认发 SIGPIPE 终止进程。这和你的业务逻辑无关,但会让服务“静默死亡”,连日志都来不及打。
必须在程序启动最早期(if __name__ == "__main__": 之后立刻)做两件事:
- 调用
signal.signal(signal.SIGPIPE, signal.SIG_IGN) - 如果用了多线程(比如
concurrent.futures.ThreadPoolExecutor),还得用pthread_sigmask在每个线程屏蔽SIGPIPE,否则子线程仍可能 crash
别依赖“没遇到过就等于不存在”——工业现场或高并发压测时,SIGPIPE 出现概率极高。
异步资源 close() 必须配对 wait_closed(),且不能只靠 finally
aiohttp.ClientSession、aiomysql.Pool、aioredis.Redis 这些对象的 close() 是异步方法,只调用它不等于资源已释放。常见错误是:
- 在
finally块里只写session.close(),没await session.wait_closed() - 以为
__aexit__会自动等完,但 signal handler 触发时上下文管理器早已退出作用域
真正安全的做法是:把 close() + wait_closed() 封装成一个协程,在退出流程中统一 await;并且确保这个协程本身也被 create_task() 启动后纳入 task 清理列表。
多线程+信号注册必须限定主线程,否则 handler 不生效
signal.signal() 和 loop.add_signal_handler() 都只对主线程有效。如果你在子线程里注册,或者用 threading.Thread 启动了后台工作线程,信号来了主线程收不到,整个程序就硬退出。
验证方式很简单:启动后用 kill -TERM $(pidof python) 测试,观察是否打印出 “shutting down”。没反应?八成是信号注册跑错线程了。
补救措施只有两个:
- 所有信号注册逻辑必须放在主线程,且早于任何子线程启动
- 如果必须跨线程通知,用
threading.Event或queue.Queue从 signal handler 发送退出指令,由主循环轮询检查
真正的难点不在代码怎么写,而在于退出路径是否覆盖所有资源类型——socket、数据库连接、线程池、定时器、文件句柄……漏掉任意一个,都可能让服务下次启动失败或资源耗尽。别信“差不多就行”,生产环境里,差一点就是故障。

















