结论:在多进程里启动 asyncio 事件循环时,应手动创建并管理事件循环,禁用 asyncio.run(),改用 loop.run_forever() + loop.add_signal_handler()。因为 asyncio.run() 会接管并重置 SIGINT/SIGTERM 处理逻辑,关闭 loop 后无法再注册信号处理器,且子进程继承父进程信号掩码未隔离,导致 Ctrl+C 或 SIGTERM 无法被捕获;Windows 不支持 add_signal_handler(),Linux/macOS 需显式设置;正确做法是每个子进程调用 asyncio.new_event_loop()、注册同步信号处理器、调用 loop.stop() 而非 close(),并在 shutdown 协程中清理所有任务和资源,同时父进程需正确处理 SIGCHLD 并等待子进程退出。

直接说结论:在多进程里启动 asyncio 事件循环时,asyncio.run() 会接管 SIGINT 并重置信号行为,导致父进程发来的 SIGTERM 或子进程自己按 Ctrl+C 都无法被正确捕获;根本解法是**每个子进程手动创建并管理自己的事件循环,禁用 asyncio.run(),改用 loop.run_forever() + loop.add_signal_handler()**。
为什么 multiprocessing + asyncio.run() 会导致信号丢失?
现象很典型:子进程启动后,按 Ctrl+C 没反应,或者父进程用 os.kill(pid, signal.SIGTERM) 发信号,子进程直接忽略、不执行清理逻辑就僵死。
原因在于:asyncio.run() 内部调用 loop.run_until_complete() 后会自动 loop.close(),并且在启动时悄悄把 SIGINT 和 SIGTERM 的 handler 替换为它自己的终止逻辑。一旦 loop 关闭,再注册信号 handler 就无效了——而 multiprocessing 的子进程默认复用父进程的信号设置,又没做隔离。
-
asyncio.run()不允许你在它运行期间调用loop.add_signal_handler()(会报RuntimeError: Event loop is running) - 子进程继承父进程的信号 mask,但未显式重置,导致
SIGCHLD等关键信号被阻塞 - Windows 下
add_signal_handler()直接抛NotImplementedError,Linux/macOS 才可用
子进程中正确注册信号 handler 的实操步骤
必须绕过 asyncio.run(),手动控制 loop 生命周期。以下是在 multiprocessing.Process 子进程里安全处理 SIGTERM 的最小可行写法:
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
import asyncio
import signal
import os
<p>def worker():</p><h1>1. 显式创建新 loop,避免 get_event_loop() 拿到已关闭或被污染的实例</h1><pre class="brush:php;toolbar:false;">loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 2. 注册同步信号 handler(不能是 async def!)
def handle_sigterm():
print(f"[pid {os.getpid()}] SIGTERM received, shutting down...")
# 3. 在 handler 里提交异步清理任务
loop.create_task(shutdown())
# 4. 停止 loop(不是 close!close 后无法再 run)
loop.stop()
loop.add_signal_handler(signal.SIGTERM, handle_sigterm)
# 5. 启动主协程(注意:不要 await,要用 create_task)
loop.create_task(main_coroutine())
# 6. 运行事件循环直到 stop() 被调用
try:
loop.run_forever()
finally:
loop.close()async def main_coroutine(): while True: await asyncio.sleep(1) print("working...")
async def shutdown(): print("running cleanup...") await asyncio.sleep(0.5) # 模拟异步关闭逻辑 print("cleanup done")
- 必须用
asyncio.new_event_loop(),不用get_event_loop()—— 后者在子进程里可能返回 None 或异常 loop -
handle_sigterm是普通函数,不是async def;里面只能调loop.create_task()和loop.stop() -
loop.run_forever()是关键:它不会自动关闭 loop,允许你多次 stop/start(适合长周期服务) - 别在 handler 里做耗时操作(如
time.sleep、文件写入),否则信号响应延迟甚至卡死
父子进程间信号传递与清理协同
单纯子进程能响应信号还不够——父进程需要确保发信号后等待其真正退出,否则可能残留僵尸进程或资源泄漏。
- 父进程发
SIGTERM后,应调用proc.join(timeout=5),超时则proc.terminate() - 子进程的
shutdown()协程里,要显式await所有 pending task,比如await asyncio.gather(*asyncio.all_tasks(loop), return_exceptions=True) - 如果子进程还开了线程池(如用
loop.run_in_executor()),需在shutdown()中调用executor.shutdown(wait=True) - 避免在子进程里使用全局 event loop 实例(如
asyncio.get_event_loop()),全部显式传参或绑定到 local loop 变量
最易被忽略的一点:子进程初始化时没调 signal.signal(signal.SIGCHLD, signal.SIG_DFL),会导致父进程收不到子进程退出通知,join() 一直阻塞。这个细节不在 asyncio 文档里,但在 Linux 多进程编程中是硬性要求。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










