You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

主线程取消run_in_executor协程失效导致程序挂起的原因

挂起问题的根本原因

核心矛盾点在于*run_in_executor提交的线程池任务无法被asyncio的cancel()方法安全终止*,结合你的代码时序,具体触发逻辑如下:

  • asyncio的cancel()只能中断协程层面的await等待逻辑,没有能力强制终止OS层面正在运行的线程。当你对等待run_in_executor返回的task调用cancel()时,只会让协程立刻抛出CancelledError,线程池里实际运行的同步逻辑不会受到任何影响,会继续在后台跑。
  • 你的代码执行到取消逻辑时的时序是:
    1. 主协程完成3次共6秒的sleep,此时子线程正阻塞在future.result()上,等待第二次提交的asyncio.sleep(5)执行完成,还没走到下一轮循环判断event.is_set()的逻辑。
    2. 你先调用event.set(),紧接着调用task.cancel(),协程层面立刻收到CancelledError,你捕获异常后打印task cancelled,主协程随即执行完毕,事件循环开始停止。
    3. 此时子线程还在等待run_coroutine_threadsafe提交的sleep任务返回结果,但事件循环已经停止,这个sleep任务永远不会被调度执行,子线程就永久阻塞在future.result()调用上,无法退出。
  • Python进程退出时,atexit钩子会默认等待所有非daemon的线程池工作线程执行结束,子线程永久阻塞就导致整个进程挂起,直到你手动发送中断信号。
为什么去掉cancel()就能正常运行

不调用cancel()时,running_bg协程会一直阻塞等待run_in_executor的任务(也就是子线程的running函数)执行完成才返回:

  1. 你调用event.set()后,子线程会等当前正在等待的asyncio.sleep(5)执行完毕,回到循环头部检测到标志位置位,执行break退出循环,子线程正常结束。
  2. 子线程结束后run_in_executor的future正常返回结果,running_bg协程执行完毕,主协程才会退出,事件循环正常关闭,不会出现悬挂线程。
场景最佳实践
  • 对于跑在线程池/进程池里的同步任务,永远不要依赖asyncio的cancel()做终止逻辑,应该用threading.Event这类跨执行单元的安全标志位通知任务主动退出,并且一定要等待executor中的任务真正执行完成后再退出主逻辑。
  • 你的ping逻辑可以做简化,避免跨线程调用asyncio协程带来的悬挂风险,直接用线程侧的等待方法即可:
def running(loop, event):
    while not event.is_set():
        print("We are in running")
        # 等待5秒,event一旦被set会立刻返回,不会出现永久阻塞
        if event.wait(timeout=5):
            break
    print("cancelling")
    return
  • 如果必须在子线程中调度asyncio协程,一定要确保在事件循环关闭前,所有跨线程提交的协程都已经执行完成,避免出现永久等待的future。

内容的提问来源于stack exchange,提问作者Ja8zyjits

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 00:01:24