← 返回题目列表

asyncio 中超时和取消任务怎么处理?CancelledError 要注意什么?

高频 困难 第 11 / 27 题 更新于 2026/07/27
timeoutcancelCancelledErrorasyncio

简化版

asyncio 里可以用 asyncio.wait_for()asyncio.timeout() 控制超时,超时时通常会取消被等待的任务。取消通过向协程注入 CancelledError 实现,协程应在 finally 中清理资源,不要随便吞掉取消异常,否则任务可能无法按预期停止。

详细版

超时示例:

async def main():
    try:
        result = await asyncio.wait_for(fetch(), timeout=3)
    except asyncio.TimeoutError:
        print("timeout")

现代写法也可以使用超时上下文:

async def main():
    try:
        async with asyncio.timeout(3):
            result = await fetch()
    except TimeoutError:
        print("timeout")

取消任务:

task = asyncio.create_task(worker())
task.cancel()

try:
    await task
except asyncio.CancelledError:
    print("cancelled")

注意点:

  • 取消不是强杀,而是在任务下一个可取消点抛出 CancelledError
  • 协程应使用 try/finally 做资源清理;
  • 不要无脑 except Exception 以为能捕获取消;
  • 不要吞掉 CancelledError 后继续假装任务正常完成;
  • 对关键清理逻辑可以了解 asyncio.shield(),但不要滥用。

完整版教学

一、异步任务为什么必须有超时

网络请求、数据库查询、第三方接口都可能卡住。如果没有超时,任务可能一直占着资源:

  • 连接不释放;
  • 队列堆积;
  • 请求一直等待;
  • 服务优雅关闭被拖住。

异步系统通常并发量更高,一个没超时的外部依赖可能拖住大量任务。

二、wait_for 的基本语义

asyncio.wait_for() 等待某个 awaitable 在指定时间内完成:

await asyncio.wait_for(fetch(), timeout=3)

如果超时,它会触发取消,并抛出超时异常。这里要理解:超时不是简单“不等了”,它还会尝试取消内部任务。

如果内部协程有清理逻辑,事件循环会给它机会处理取消。

三、asyncio.timeout 的可读性

超时上下文适合包住一段异步逻辑:

async with asyncio.timeout(5):
    user = await get_user()
    orders = await get_orders(user.id)

它表达的是“这一整段必须在 5 秒内完成”。相比每个 await 单独写 wait_for,结构更清晰。

四、取消不是强制杀死

调用:

task.cancel()

并不等于操作系统层面的强杀。它请求取消任务,任务通常会在下一个 await 点收到 CancelledError

async def worker():
    try:
        await do_work()
    finally:
        await cleanup()

这就是为什么协程要在 finally 里清理资源,比如关闭连接、释放锁、回滚状态。

五、为什么不能随便吞掉 CancelledError

错误写法:

async def worker():
    try:
        await do_work()
    except asyncio.CancelledError:
        pass

如果吞掉取消异常,外部可能以为任务正常完成,任务组或上层取消逻辑也可能被破坏。更好的做法是清理后重新抛出:

async def worker():
    try:
        await do_work()
    except asyncio.CancelledError:
        await cleanup()
        raise

这代表“我知道被取消了,清理完继续告诉上层我确实取消了”。

六、shield 的作用和风险

asyncio.shield() 可以保护某个 awaitable 不因外层取消而直接取消。它适合极少数必须完成的清理或提交动作。

但 shield 很容易让取消语义复杂化。如果所有东西都 shield,服务关闭时就会拖泥带水。面试中知道它存在即可,回答重点仍然是超时、取消、清理和异常传播。

七、常见误区与追问

机制作用风险点
wait_for给单个 awaitable 加超时超时会取消内部任务
asyncio.timeout给代码块设置超时上下文可读性更适合包住一段逻辑
CancelledError协程取消信号不应随便吞掉
shield保护内部任务不被外层取消直接影响可能造成任务继续后台运行
  • 误区:取消任务等于立刻杀掉任务。 asyncio 的取消是协作式的,任务在下一个可取消的 await 点收到 CancelledError
  • 误区:捕获 Exception 就会捕获取消。 在现代 Python 中 CancelledError 的继承关系和版本有关,面试重点是不要把取消当普通业务异常吞掉。
  • 误区:shield 能让任务永远不受取消影响。 shield 只是隔离外层取消对内部 awaitable 的直接传播,内部任务仍可能因自身逻辑或其他路径被取消。
  • 追问:为什么必须给外部调用加超时? 网络、RPC、数据库调用没有超时会长期占住连接、协程和业务请求,最终造成级联阻塞。
  • 追问:取消时 finally 还会执行吗? 会,协程应在 finally 中释放连接、锁、文件等资源,然后通常继续向外传播取消。
  • 追问:wait_for 和 asyncio.timeout 怎么选? 单个 awaitable 可用 wait_for;一段包含多次 await 的业务流程用 asyncio.timeout 更清楚。

易错点:超时通常通过取消实现,所以写异步代码时要把“释放资源”和“不要吞 CancelledError”放在同一套思路里。

八、加强记忆

超时是为了给等待设置边界,取消是为了让任务有序停止。wait_forasyncio.timeout 负责超时控制,task.cancel() 负责请求取消,CancelledError 是协程收到的停止信号。清理资源用 finally,取消异常清理后通常要继续抛出,不要悄悄吞掉。