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

搭配aiohttp WebSocket使用时asyncio.sleep为何会冻结运行态任务且无法取消?

问题核心成因
  • 调度时序冲突导致取消信号被阻塞:当前服务端的处理逻辑是,收到客户端的close消息后,先执行await ws.close(),再调用send_task的cancel方法。aiohttp的WebSocket在执行close流程时,会持有连接对应的IO写锁,等待对端返回close帧完成关闭握手。当send_to_socket任务正卡在asyncio.sleep的挂起点时,哪怕cancel()是同步调用的取消接口,实际的取消动作(也就是在协程栈中抛出CancelledError)需要事件循环调度到sleep对应的定时器Future时才能触发——此时事件循环正被ws.close()的IO等待逻辑占住调度通道,导致取消信号根本没法投递到send任务的协程栈里,就出现了“调用了cancel但任务完全冻结”的现象。
  • 对cancel()的非阻塞特性认知偏差:asyncio.Task.cancel()本身只是向目标任务提交一个取消请求,不会立刻中断协程执行,生效必须依赖目标协程下一次被事件循环唤醒时,才会在协程栈中抛出对应的取消异常。如果提交取消请求后,立刻被后续的阻塞IO操作挡住了事件循环的调度通道,取消请求就会一直积压在事件队列里,完全无法生效。
  • 退出判断条件单一:send_to_socket任务只靠ws.closed作为循环退出的判断依据,而ws.closed的状态更新是和WebSocket的IO流程强绑定的,当连接被客户端异常关闭、IO流程被打断时,这个状态不一定能被卡在sleep上的任务及时感知。如果没有独立的退出信号做兜底,任务很容易进入永久挂起状态。

代码注释里提到的“去掉asyncio.sleep就正常”的现象,本质是因为没有sleep时,send任务会以高优先级持续在事件循环中运行,每次发完消息立刻检查ws的关闭状态,一旦ws被关闭就主动break退出循环,完全不需要靠cancel打断挂起状态,所以不会出现调度阻塞的问题。

针对性修复方案
  • 调整逻辑执行优先级:收到客户端的close消息时,第一时间调用send_task.cancel()提交取消请求,再执行ws.close()的关闭流程,避免取消请求被后续的IO关闭操作阻塞。
  • 给取消流程增加确认等待:调用cancel()后,使用await asyncio.wait_for(asyncio.gather(send_task, return_exceptions=True), timeout=1),强制给事件循环留出调度时间,让取消信号能被send任务正常处理,避免请求积压。
  • 用共享事件替代纯状态判断:在两个任务间传入一个共享的asyncio.Event作为退出信号,当需要结束连接时直接设置这个事件,send任务每次循环开头检查事件状态,一旦收到退出信号就立刻终止循环,不依赖ws的IO状态做判断。
  • 替换任务等待逻辑:放弃直接asyncio.gather两个任务的写法,改用asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)模式,只要任意一个任务触发退出,就立刻取消另一个关联任务,避免出现任务间互相等待的死锁。
  • 给延迟等待加状态检查:将固定时长的asyncio.sleep替换为分片等待逻辑,每等待10ms就检查一次ws状态和任务取消标记,累计到0.5秒再执行下一次发送,避免任务长时间挂起在独立sleep上无法及时响应退出信号。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.23 08:27:02