Python脚本如何取消超时未完成协程

wen python案例 25

本文目录导读:

Python脚本如何取消超时未完成协程

  1. 目录导读
  2. 协程超时问题的本质
  3. 内置工具:asyncio.wait_for 的正确用法
  4. 手动取消:Task 对象与 cancel() 方法详解
  5. 超时未完成的终极兜底:shield 与超时组合
  6. 实战案例:爬虫场景下的协程超时取消
  7. 常见问答 Q&A

Python协程超时未完成?一劳永逸的取消方案与实战指南

目录导读

  1. 协程超时问题的本质
  2. 内置工具:asyncio.wait_for 的正确用法
  3. 手动取消:Task 对象与 cancel() 方法详解
  4. 超时未完成的终极兜底:shield 与超时组合
  5. 实战案例:爬虫场景下的协程超时取消
  6. 常见问答 Q&A

协程超时问题的本质

在异步编程中,协程可能因网络延迟、外部服务响应慢或代码逻辑错误而“挂起”超过预期时间,Python 的 asyncio 库虽然提供了协程调度,但不会主动中断未完成的协程,如果不处理超时,协程将永远占用事件循环资源,最终导致整个应用阻塞、内存泄漏甚至崩溃。

核心痛点

  • await 一个协程时,没有内置的“超时取消”机制。
  • 直接用 time.sleep() 无法撤销协程的执行。
  • 多人协作时,未释放协程可能引发级联故障。

解决方案思路
通过 asyncio.wait_for() 设置超时,或使用 Task.cancel() 主动撤销,以下从易到难,逐步拆解。


内置工具:asyncio.wait_for 的正确用法

基础语法

import asyncio
async def long_running_task():
    await asyncio.sleep(10)
    return "完成"
async def main():
    try:
        result = await asyncio.wait_for(long_running_task(), timeout=3.0)
        print(result)
    except asyncio.TimeoutError:
        print("协程超时,已取消")

核心原理
wait_for 内部会创建 Task 对象,在超时后自动调用 task.cancel(),被取消的协程会在下一个可暂停点(如 await)抛出 CancelledError

注意事项

  • 超时不能为负数或零:会导致立即引发 ValueError
  • 不会杀死线程:如果协程内部有 CPU 密集型代码且无 await,取消将不生效。
  • 异常处理:务必捕获 asyncio.TimeoutErrorCancelledError,否则程序会崩溃。

搜索引擎优化要点

  • 对比 asyncio.run() 内使用 wait_for:主协程自身也会被管理,但外层超时更可靠。
  • 性能:每次 wait_for 会创建新 Task,大量高频调用建议用 asyncio.ensure_future 预创建。

手动取消:Task 对象与 cancel() 方法详解

当需要更精细控制(如取消多个协程、取消后执行清理)时,手动管理 Task 更灵活。

示例代码

async def main():
    # 预创建 Task
    task = asyncio.create_task(long_running_task())
    # 设置 3 秒超时
    try:
        await asyncio.wait([task], timeout=3.0)
        if not task.done():
            task.cancel()
            # 等待取消完成
            try:
                await task
            except asyncio.CancelledError:
                print("任务已手动取消")
    except Exception as e:
        print(f"异常: {e}")

关键方法

方法 作用
task.cancel() 请求取消,协程内需处理 CancelledError
task.cancelled() 检查是否已取消
task.done() 检查是否完成(含取消)

深层陷阱

  • 重复取消:对已取消的 Task 再次取消不会报错,但无效果。
  • 取消后 await:取消的 Task 在 await 时会抛出 CancelledError,需捕获。
  • 清理工作:在协程内部用 try-finally 释放资源。

超时未完成的终极兜底:shield 与超时组合

有时并不想真的取消协程(例如正在写入数据库),但外层必须超时返回。asyncio.shield() 可以保护协程不被取消。

场景

async def write_to_db():
    # 正在执行重要写入
    await asyncio.sleep(5)
    return "写入成功"
async def main():
    task = asyncio.create_task(write_to_db())
    try:
        # 保护该任务不被超时取消
        shield_task = asyncio.shield(task)
        result = await asyncio.wait_for(shield_task, timeout=2.0)
        print(result)
    except asyncio.TimeoutError:
        print("外层超时,但写入仍在后台继续")
        # 等待后台任务真正完成
        real_result = await task
        print(f"后台最终结果: {real_result}")

实战建议

  • 不要滥用 shield:只有确实需要“后台完成”时才用,否则拖慢整体。
  • 超时后同步等待:上例中 await task 会再次阻塞,适合写入型任务。
  • 风险:若后台任务永远不完成,依然会泄漏。

实战案例:爬虫场景下的协程超时取消

假设用 aiohttp 并发抓取 100 个 URL,要求每 5 秒未响应则跳过。

完整代码

import asyncio
import aiohttp
from asyncio import TimeoutError
async def fetch(session, url):
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=3)) as resp:
            return await resp.text()
    except asyncio.CancelledError:
        print(f"{url} 被取消")
        raise
    except Exception as e:
        return f"{url} 错误: {e}"
async def main():
    urls = [f"http://example.com/page_{i}" for i in range(100)]
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
        # 整体超时控制
        done, pending = await asyncio.wait(tasks, timeout=5.0)
        # 取消未完成的协程
        for task in pending:
            task.cancel()
        # 收集结果
        results = []
        for task in done:
            try:
                results.append(await task)
            except CancelledError:
                pass
        print(f"完成 {len(results)} 个请求")
asyncio.run(main())

绩效优化

  • 使用 asyncio.wait()return_when=FIRST_EXCEPTION 可提前终止。
  • 为每个请求单独设置 ClientTimeout + 外层 wait 双重保险。

常见问答 Q&A

Q:协程取消后,内部代码还会执行吗?
A:不会,取消会在下一个 await 点抛出 CancelledError,若协程内无 await,则不会被中断(如纯 CPU 循环)。

Q:如何优雅释放取消后的资源?
A:在协程内部用 try-finally 包裹,finally 块中关闭文件、数据库连接等。

Q:asyncio.wait_forTask.cancel() 哪种更推荐?
A:简单场景用 wait_for;需要批量取消、后台执行或资源清理时,手动管理 Task。

Q:Pypy 或 asyncio 版本差异重要吗?
A:基础 API 在 Python 3.7+ 稳定,但在 3.11 中 Task.cancel() 增加了 msg 参数,建议保持最新。

Q:超时未完成协程会导致内存泄漏吗?
A:若未取消,Task 对象会持有所有引用,无法被 GC 回收,长期运行必泄漏,务必取消或使用 asyncio.wait() 等待完成。


取消超时未完成的协程不是“用一把尖刀杀死进程”,而是通过 CancelledError 机制优雅终止,选择 wait_for 作为第一道防线,手动 Task 管理为第二道,shield 作为特殊场景的后备,遵循“尽早创建、及时取消、资源释放”三原则,即可在大型异步项目中稳定运行。

抱歉,评论功能暂时关闭!