{T}

事件循环与协程深入

概述

是什么

事件循环(Event Loop)是 asyncio 的核心引擎,负责调度和执行协程、处理 IO 回调、管理定时器。协程(Coroutine)则是可暂停和恢复执行的函数,由事件循环驱动运行。两者共同构成了 Python 异步编程的运行时基础。

为什么

很多开发者会写 async/await,却不理解事件循环如何调度协程、Task 何时被挂起与恢复、gatherwait 有何本质区别。这种"知其然不知其所以然"的状态,在面对以下场景时会暴露问题:

  • 异步代码中混入同步阻塞调用,导致整个服务卡死
  • Task 异常被静默吞掉,排查时毫无头绪
  • 手动管理事件循环时出现"no current event loop"错误
  • 需要与同步库交互时不知如何正确使用 run_in_executor

理解事件循环与协程的底层机制,是从"会写异步代码"到"能设计异步系统"的关键跨越。

怎么做

本文将从事件循环的工作原理出发,逐步深入 asyncio.run() 内部机制、手动循环管理、Task 调度与生命周期、并发原语对比、超时处理、协程与线程交互,最后通过实战场景和常见陷阱巩固理解。

知识定位

图表渲染中…
阅读建议

本文假设你已掌握 async/await 基本语法和 asyncio.run() 的基本用法。如果你是异步编程初学者,建议先阅读 asyncio 基础。本文所有代码基于 Python 3.10+,部分特性需要 Python 3.11+

事件循环工作原理

核心概念

事件循环本质上是一个单线程的任务调度器,它不断重复以下步骤:

  1. 检查是否有就绪的 IO 操作(通过操作系统的多路复用机制,如 epoll/kqueue)
  2. 执行就绪的回调或恢复挂起的协程
  3. 处理定时器到期事件
  4. 重复上述过程,直到没有待处理任务

事件循环处理流程

图表渲染中…

事件循环的四个阶段

每个事件循环迭代(称为一个 tick)包含以下阶段:

图表渲染中…
阶段说明典型操作
处理就绪回调执行上一轮 IO 完成后排队的回调loop.call_soon() 注册的回调
处理 IO 事件通过 selector 检查哪些 IO 操作就绪socket 可读/可写、文件描述符事件
处理定时器检查是否有到期的定时任务loop.call_later() 注册的定时回调
空闲等待如果没有就绪事件,阻塞等待selector.select(timeout)

事件循环的底层实现

Python 在不同平台上使用不同的 IO 多路复用机制:

平台IO 多路复用机制说明
Linuxepoll边缘触发或水平触发,性能最优
macOSkqueue类似 epoll,支持文件系统事件
WindowsIOCP (I/O Completion Ports)通过 ProactorEventLoop 实现
通用select兼容性最好,但有文件描述符数量限制
Windows 注意事项

在 Windows 上,SelectorEventLoop 不支持 socket 以外的 IO 操作。Python 3.8+ 默认使用 ProactorEventLoop,它基于 IOCP 实现,支持子进程和管道操作。如果你的代码需要跨平台运行,务必注意这一差异。

代码示例:观察事件循环行为

python
import asyncio
import time

async def task(name: str, delay: float) -> str:
    print(f"[{time.strftime('%H:%M:%S')}] {name} 开始执行")
    await asyncio.sleep(delay)  # 模拟 IO 操作
    print(f"[{time.strftime('%H:%M:%S')}] {name} 执行完毕")
    return f"{name} 的结果"

async def main():
    # 创建多个 Task,观察事件循环如何交替调度
    t1 = asyncio.create_task(task("任务A", 2))
    t2 = asyncio.create_task(task("任务B", 1))
    t3 = asyncio.create_task(task("任务C", 3))

    # 事件循环会在 Task 挂起时切换执行其他 Task
    results = await asyncio.gather(t1, t2, t3)
    print(f"所有结果: {results}")

asyncio.run(main())

输出(时间线):

code
[14:30:01] 任务A 开始执行
[14:30:01] 任务B 开始执行
[14:30:01] 任务C 开始执行
[14:30:02] 任务B 执行完毕    # 1秒后最先完成
[14:30:03] 任务A 执行完毕    # 2秒后完成
[14:30:04] 任务C 执行完毕    # 3秒后完成
所有结果: ['任务A 的结果', '任务B 的结果', '任务C 的结果']

asyncio.run() 内部机制

asyncio.run() 做了什么

asyncio.run() 是 Python 3.7+ 推荐的启动异步程序的入口函数。它封装了事件循环的完整生命周期管理:

图表渲染中…

源码级理解

asyncio.run() 的核心逻辑(简化版):

python
# asyncio.runners 模块简化实现
def run(main, *, debug=None):
    # 1. 安全校验:确保没有已运行的事件循环
    if events._get_running_loop() is not None:
        raise RuntimeError("asyncio.run() cannot be called from a running event loop")

    # 2. 确保传入的是协程
    if not coroutines.iscoroutine(main):
        raise ValueError("a coroutine was expected, got {!r}".format(main))

    # 3. 创建新事件循环
    loop = events.new_event_loop()

    try:
        events.set_event_loop(loop)
        if debug is not None:
            loop.set_debug(debug)
        # 4. 运行主协程
        return loop.run_until_complete(main)
    finally:
        try:
            # 5. 取消所有未完成的 Task
            _cancel_all_tasks(loop)
            # 6. 运行直到所有 Task 彻底结束
            loop.run_until_complete(loop.shutdown_asyncgens())
            # Python 3.9+: 关闭延迟回调
            loop.run_until_complete(loop.shutdown_default_executor())
        finally:
            # 7. 关闭事件循环
            events.set_event_loop(None)
            loop.close()

关键行为总结

行为说明
每次调用创建新循环asyncio.run() 每次调用都会创建一个全新的事件循环,运行完毕后关闭
不允许嵌套调用在已运行的事件循环中再次调用会抛出 RuntimeError
自动清理 Task主协程结束后,自动取消所有未完成的 Task
关闭异步生成器确保所有 async for 相关的异步生成器被正确关闭
关闭线程池Python 3.9+ 会等待默认 executor 的线程结束
不要在 Jupyter 中使用 asyncio.run()

Jupyter Notebook 自身已经运行了一个事件循环,直接调用 asyncio.run() 会报错。在 Jupyter 中直接 await 协程即可,或使用 nest_asyncio 库。

手动管理事件循环

虽然 asyncio.run() 是推荐的入口,但在某些场景下需要手动管理事件循环,例如:集成到已有框架、编写测试、需要精细控制循环生命周期。

获取事件循环

python
import asyncio

# 方式1: 获取当前线程的事件循环(如果没有则创建)
loop = asyncio.get_event_loop()

# 方式2: 获取当前正在运行的事件循环(如果没有则抛出异常)
try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    print("没有正在运行的事件循环")

# 方式3: 创建新的事件循环(不设置为当前循环)
loop = asyncio.new_event_loop()

# 方式4: 创建并设置为当前线程的事件循环
loop = asyncio.set_event_loop(asyncio.new_event_loop())
API行为适用场景
get_event_loop()获取当前循环,无则创建Python 3.10 之前的旧代码
get_running_loop()获取正在运行的循环,无则报错在协程内部获取当前循环
new_event_loop()创建新循环,不设置需要独立循环时
set_event_loop()设置当前线程的循环手动管理循环时
Python 3.10+ 的行为变化

从 Python 3.10 开始,在没有运行中事件循环的上下文中调用 asyncio.get_event_loop() 会发出 DeprecationWarning,未来版本将改为抛出异常。推荐使用 asyncio.get_running_loop() 或显式创建循环。

run_until_complete

run_until_complete() 接受一个协程或 Future,运行事件循环直到该协程完成,并返回结果:

python
import asyncio

async def fetch_data() -> str:
    await asyncio.sleep(1)
    return "数据获取完成"

# 手动管理事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

try:
    result = loop.run_until_complete(fetch_data())
    print(result)  # 数据获取完成
finally:
    loop.close()

run_forever

run_forever() 让事件循环持续运行,直到显式调用 loop.stop()

python
import asyncio

async def periodic_task():
    """每2秒执行一次的周期任务"""
    count = 0
    while True:
        count += 1
        print(f"周期任务第 {count} 次执行")
        await asyncio.sleep(2)

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

# 启动周期任务
task = loop.create_task(periodic_task())

# 5秒后停止循环
loop.call_later(5, loop.stop)

try:
    loop.run_forever()
finally:
    task.cancel()  # 取消未完成的任务
    loop.run_until_complete(task)  # 等待取消完成
    loop.close()

手动循环管理对比

方法阻塞行为退出条件返回值典型场景
run_until_complete(coro)阻塞直到协程完成协程返回协程结果运行单个协程
run_forever()持续阻塞loop.stop()长期运行的服务
asyncio.run(coro)阻塞直到协程完成协程返回+清理协程结果标准入口(推荐)

在同步代码中调用异步函数

python
import asyncio

async def async_operation() -> str:
    await asyncio.sleep(1)
    return "异步操作完成"

def sync_caller() -> str:
    """从同步代码中调用异步函数的正确方式"""
    try:
        # 优先尝试获取已运行的循环(如在 Jupyter 中)
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None

    if loop and loop.is_running():
        # 已有运行中的循环,使用 nest_asyncio 或线程方案
        import concurrent.futures
        with concurrent.futures.ThreadPoolExecutor() as pool:
            future = pool.submit(asyncio.run, async_operation())
            return future.result()
    else:
        # 没有运行中的循环,直接创建
        return asyncio.run(async_operation())

result = sync_caller()
print(result)  # 异步操作完成

Task 调度与生命周期

Task 是什么

Task 是事件循环中对协程的包装,是可调度的执行单元。协程本身只是定义,必须被包装成 Task 后才能被事件循环调度执行。

python
import asyncio

async def my_coroutine():
    await asyncio.sleep(1)
    return "完成"

# 协程 vs Task 的区别
coro = my_coroutine()       # 协程对象,尚未执行
task = asyncio.create_task(my_coroutine())  # Task,已被调度到事件循环

Task 生命周期状态图

图表渲染中…

创建 Task 的方式

python
import asyncio

async def work(name: str) -> str:
    await asyncio.sleep(1)
    return f"{name} 完成"

async def main():
    # 方式1: asyncio.create_task() — 推荐(Python 3.7+)
    task1 = asyncio.create_task(work("任务1"), name="my-task-1")

    # 方式2: loop.create_task() — 底层 API
    loop = asyncio.get_running_loop()
    task2 = loop.create_task(work("任务2"))

    # 方式3: asyncio.ensure_future() — 兼容旧代码
    task3 = asyncio.ensure_future(work("任务3"))

    # 方式4: Python 3.11+ TaskGroup — 结构化并发
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(work("TG任务1"))
        t2 = tg.create_task(work("TG任务2"))
    # TaskGroup 退出时自动等待所有 Task 完成

    results = await asyncio.gather(task1, task2, task3)
    print(results)
创建方式Python 版本推荐度说明
asyncio.create_task()3.7+推荐最常用,自动绑定当前循环
asyncio.TaskGroup3.11+推荐结构化并发,异常处理更安全
loop.create_task()全版本特定场景需要指定循环时使用
asyncio.ensure_future()全版本不推荐兼容旧代码,行为不够直观

Task 取消机制

python
import asyncio

async def long_running_task():
    try:
        print("任务开始")
        await asyncio.sleep(10)  # 模拟长时间 IO
        print("任务完成")  # 这行不会执行
    except asyncio.CancelledError:
        print("任务被取消,执行清理")
        raise  # 重要:重新抛出 CancelledError
    finally:
        print("清理资源")

async def main():
    task = asyncio.create_task(long_running_task())

    # 等待一小段时间后取消
    await asyncio.sleep(1)
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print("主协程感知到任务被取消")

asyncio.run(main())

输出:

code
任务开始
任务被取消,执行清理
清理资源
主协程感知到任务被取消
CancelledError 处理要点
  1. Python 3.9+CancelledError 继承自 BaseException 而非 Exception,不会被 except Exception 捕获
  2. 如果你在 except CancelledError 中不重新抛出,Task 不会被标记为已取消
  3. 始终在 finally 块中释放资源(关闭连接、释放锁等)

TaskGroup 结构化并发(Python 3.11+)

python
import asyncio

async def fetch(url: str) -> str:
    await asyncio.sleep(1)
    if "error" in url:
        raise ValueError(f"获取 {url} 失败")
    return f"数据来自 {url}"

async def main():
    urls = ["http://api1.com", "http://error.com", "http://api2.com"]

    try:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(fetch(url)) for url in urls]
    except* ValueError as eg:
        # ExceptionGroup 语法:只处理 ValueError 类型的异常
        print(f"部分任务失败: {eg.exceptions}")
    else:
        results = [t.result() for t in tasks]
        print(results)

asyncio.run(main())

TaskGroup 的核心优势:

特性TaskGroupgather
任一 Task 失败时自动取消其余所有 Task默认等待全部完成(return_exceptions=False 时抛异常但不取消其他)
异常报告ExceptionGroup,保留所有异常只抛出第一个异常
结构化退出时保证所有 Task 已结束需要手动管理
取消传播父协程取消时自动取消子 Task需要手动处理

asyncio.gather vs asyncio.wait 详细对比

基本用法

python
import asyncio
import time

async def fetch(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name} 完成"

async def demo_gather():
    """gather: 按顺序收集结果"""
    results = await asyncio.gather(
        fetch("A", 2),
        fetch("B", 1),
        fetch("C", 3),
    )
    print(f"gather 结果: {results}")
    # 输出: gather 结果: ['A 完成', 'B 完成', 'C 完成']
    # 注意:结果顺序与传入顺序一致,而非完成顺序

async def demo_wait():
    """wait: 获取完成和未完成的 Task 集合"""
    tasks = {
        asyncio.create_task(fetch("A", 2)),
        asyncio.create_task(fetch("B", 1)),
        asyncio.create_task(fetch("C", 3)),
    }
    done, pending = await asyncio.wait(tasks)
    print(f"完成: {[t.result() for t in done]}")
    # 输出顺序不确定,取决于完成顺序

asyncio.run(demo_gather())
asyncio.run(demo_wait())

完整对比表

特性asyncio.gather()asyncio.wait()
输入类型协程或 FutureTask 或 Future(不接受裸协程)
返回值结果列表(按输入顺序)(done_set, pending_set) 元组
结果顺序与输入顺序一致与完成顺序一致(无序)
异常处理return_exceptions=True 时返回异常对象而非抛出异常 Task 归入 done 集合
取消行为gather 自身被取消时取消所有子 Task需手动取消 pending 集合
完成条件等待全部完成支持 FIRST_COMPLETEDFIRST_EXCEPTION
适用场景并行请求,收集所有结果需要按完成条件控制流程
Python 版本3.4+3.4+

wait 的返回策略

python
import asyncio

async def demo_wait_strategies():
    async def fast():
        await asyncio.sleep(0.1)
        return "fast"

    async def medium():
        await asyncio.sleep(0.5)
        return "medium"

    async def slow():
        await asyncio.sleep(2.0)
        return "slow"

    async def failing():
        await asyncio.sleep(0.3)
        raise ValueError("出错了")

    # 策略1: FIRST_COMPLETED — 任一完成即返回
    tasks = {asyncio.create_task(fast()),
             asyncio.create_task(medium()),
             asyncio.create_task(slow())}
    done, pending = await asyncio.wait(
        tasks, return_when=asyncio.FIRST_COMPLETED
    )
    print(f"FIRST_COMPLETED: 完成 {len(done)}, 待处理 {len(pending)}")
    # 输出: FIRST_COMPLETED: 完成 1, 待处理 2
    for t in pending:
        t.cancel()
    await asyncio.wait(pending)  # 等待取消完成

    # 策略2: FIRST_EXCEPTION — 第一个异常或全部完成
    tasks = {asyncio.create_task(fast()),
             asyncio.create_task(failing()),
             asyncio.create_task(slow())}
    done, pending = await asyncio.wait(
        tasks, return_when=asyncio.FIRST_EXCEPTION
    )
    print(f"FIRST_EXCEPTION: 完成 {len(done)}, 待处理 {len(pending)}")
    for t in pending:
        t.cancel()
    await asyncio.wait(pending)

    # 策略3: ALL_COMPLETED — 全部完成(默认)
    tasks = {asyncio.create_task(fast()),
             asyncio.create_task(medium())}
    done, pending = await asyncio.wait(tasks)  # 默认 ALL_COMPLETED
    print(f"ALL_COMPLETED: 完成 {len(done)}, 待处理 {len(pending)}")
    # 输出: ALL_COMPLETED: 完成 2, 待处理 0

asyncio.run(demo_wait_strategies())

异常处理对比

python
import asyncio

async def success():
    return "成功"

async def failure():
    raise ValueError("失败")

async def demo_exception_handling():
    # gather: return_exceptions=True 捕获异常
    results = await asyncio.gather(
        success(),
        failure(),
        success(),
        return_exceptions=True,
    )
    print(f"gather 结果: {results}")
    # 输出: ['成功', ValueError('失败'), '成功']

    # gather: return_exceptions=False(默认)抛出第一个异常
    try:
        await asyncio.gather(success(), failure(), success())
    except ValueError as e:
        print(f"gather 抛出异常: {e}")

    # wait: 异常 Task 归入 done 集合
    tasks = {
        asyncio.create_task(success()),
        asyncio.create_task(failure()),
    }
    done, pending = await asyncio.wait(tasks)
    for t in done:
        if t.exception():
            print(f"wait 发现异常: {t.exception()}")
        else:
            print(f"wait 正常结果: {t.result()}")

asyncio.run(demo_exception_handling())

选择决策树

图表渲染中…

超时处理

asyncio.wait_for

wait_for() 为协程设置超时,超时后自动取消协程:

python
import asyncio

async def slow_operation():
    await asyncio.sleep(10)
    return "完成"

async def demo_wait_for():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=2.0)
        print(result)
    except asyncio.TimeoutError:
        print("操作超时!")

asyncio.run(demo_wait_for())
# 输出: 操作超时!

asyncio.timeout(Python 3.11+)

asyncio.timeout 是上下文管理器形式的超时控制,更灵活:

python
import asyncio

async def demo_timeout():
    async with asyncio.timeout(3.0):
        # 这里的所有操作共享同一个超时限制
        await asyncio.sleep(1)  # 剩余 2 秒
        await asyncio.sleep(1)  # 剩余 1 秒
        await asyncio.sleep(1)  # 超时!
    # 如果到达这里,说明所有操作在 3 秒内完成

async def demo_timeout_nested():
    """嵌套超时:内层超时先触发"""
    try:
        async with asyncio.timeout(5.0):   # 外层 5 秒
            async with asyncio.timeout(2.0):  # 内层 2 秒
                await asyncio.sleep(10)       # 内层先超时
    except asyncio.TimeoutError:
        print("内层超时触发")

asyncio.run(demo_timeout_nested())
# 输出: 内层超时触发

超时处理方式对比

特性wait_for()asyncio.timeout
Python 版本3.4+3.11+
形式函数调用上下文管理器
作用范围单个协程代码块内所有操作
嵌套支持不支持支持,内层优先
超时后行为取消协程并抛出 TimeoutError取消代码块内所有 Task
灵活性较低较高,可控制多步操作的总时间

实用超时模式

python
import asyncio
from typing import Any

async def fetch_with_retry(
    url: str,
    timeout: float = 5.0,
    retries: int = 3,
) -> Any:
    """带重试的超时请求"""
    last_error = None

    for attempt in range(retries):
        try:
            async with asyncio.timeout(timeout):
                # 模拟 HTTP 请求
                await asyncio.sleep(2)  # 模拟网络延迟
                return f"数据来自 {url}"
        except asyncio.TimeoutError:
            last_error = TimeoutError(f"第 {attempt + 1} 次请求超时")
            print(f"第 {attempt + 1} 次请求 {url} 超时,重试中...")

    raise last_error

async def main():
    try:
        result = await fetch_with_retry("http://slow-api.com", timeout=1.0)
    except TimeoutError as e:
        print(f"所有重试失败: {e}")

asyncio.run(main())

协程与线程交互

为什么需要线程交互

asyncio 是单线程的,但很多场景需要与同步代码交互:

  • 调用不支持异步的第三方库(如 requestssubprocess
  • 执行 CPU 密集型计算
  • 集成到已有的同步框架中

run_in_executor

run_in_executor() 将同步函数提交到线程池执行,避免阻塞事件循环:

python
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor

def blocking_io() -> str:
    """模拟阻塞 IO 操作"""
    time.sleep(2)  # 同步阻塞
    return "IO 完成"

def cpu_intensive(n: int) -> int:
    """模拟 CPU 密集型计算"""
    return sum(i * i for i in range(n))

async def demo_executor():
    loop = asyncio.get_running_loop()

    # 使用默认线程池执行阻塞 IO
    result = await loop.run_in_executor(None, blocking_io)
    print(f"阻塞 IO 结果: {result}")

    # 使用自定义线程池
    with ThreadPoolExecutor(max_workers=4) as pool:
        result = await loop.run_in_executor(pool, cpu_intensive, 10_000_000)
        print(f"CPU 计算结果: {result}")

asyncio.run(demo_executor())

asyncio.to_thread(Python 3.9+)

to_thread()run_in_executor() 的高层封装,更简洁:

python
import asyncio
import time

def blocking_call(url: str) -> str:
    """同步阻塞函数"""
    time.sleep(1)
    return f"来自 {url} 的数据"

async def demo_to_thread():
    # to_thread: 简洁的线程调用方式
    result = await asyncio.to_thread(blocking_call, "http://api.com")
    print(result)

    # 对比 run_in_executor
    loop = asyncio.get_running_loop()
    result = await loop.run_in_executor(None, blocking_call, "http://api.com")
    print(result)

asyncio.run(demo_to_thread())

run_in_executor vs to_thread 对比

特性run_in_executorto_thread
Python 版本3.5+3.9+
调用方式loop.run_in_executor(pool, fn, *args)await asyncio.to_thread(fn, *args, **kwargs)
自定义线程池支持不支持(使用默认线程池)
关键字参数不支持(需 functools.partial支持
获取 loop需要先获取 loop自动获取
适用场景需要自定义线程池或 ProcessPoolExecutor简单场景,快速包装同步函数

使用 ProcessPoolExecutor 处理 CPU 密集型任务

python
import asyncio
from concurrent.futures import ProcessPoolExecutor

def heavy_computation(n: int) -> int:
    """CPU 密集型计算,适合多进程"""
    total = 0
    for i in range(n):
        total += i * i
    return total

async def demo_process_pool():
    loop = asyncio.get_running_loop()

    # 使用进程池,绕过 GIL
    with ProcessPoolExecutor() as pool:
        # 并行执行多个计算任务
        results = await asyncio.gather(
            loop.run_in_executor(pool, heavy_computation, 5_000_000),
            loop.run_in_executor(pool, heavy_computation, 5_000_000),
            loop.run_in_executor(pool, heavy_computation, 5_000_000),
        )
        print(f"计算结果: {results}")

asyncio.run(demo_process_pool())

线程安全地调度协程

从其他线程向事件循环提交协程:

python
import asyncio
import threading

async def background_task(name: str):
    print(f"[{threading.current_thread().name}] {name} 开始")
    await asyncio.sleep(1)
    print(f"[{threading.current_thread().name}] {name} 完成")
    return f"{name} 结果"

def thread_worker(loop: asyncio.AbstractEventLoop):
    """从非事件循环线程安全地提交协程"""
    # asyncio.run_coroutine_threadsafe: 线程安全的协程提交
    future = asyncio.run_coroutine_threadsafe(
        background_task("来自线程的任务"), loop
    )
    # 可以等待结果(阻塞当前线程)
    result = future.result(timeout=5)
    print(f"线程获取结果: {result}")

async def main():
    loop = asyncio.get_running_loop()

    # 启动一个普通线程,从线程中提交协程到事件循环
    thread = threading.Thread(
        target=thread_worker, args=(loop,), daemon=True
    )
    thread.start()

    # 事件循环继续处理其他任务
    await asyncio.sleep(0.5)
    print("事件循环继续工作...")

    thread.join()

asyncio.run(main())

实战场景

场景一:优雅的异步任务管理器

构建一个支持并发控制、超时、重试和优雅关闭的任务管理器:

python
import asyncio
import logging
from typing import Any, Callable, Coroutine

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class AsyncTaskManager:
    """异步任务管理器

    特性:
    - 并发控制(Semaphore)
    - 超时处理
    - 自动重试
    - 优雅关闭
    - 结果收集
    """

    def __init__(
        self,
        max_concurrency: int = 10,
        default_timeout: float = 30.0,
        default_retries: int = 3,
    ):
        self._semaphore = asyncio.Semaphore(max_concurrency)
        self._default_timeout = default_timeout
        self._default_retries = default_retries
        self._tasks: list[asyncio.Task] = []
        self._results: dict[str, Any] = {}
        self._errors: dict[str, Exception] = {}

    async def submit(
        self,
        name: str,
        coro_func: Callable[..., Coroutine],
        *args: Any,
        timeout: float | None = None,
        retries: int | None = None,
        **kwargs: Any,
    ) -> None:
        """提交一个任务到管理器"""
        task = asyncio.create_task(
            self._run_with_control(
                name, coro_func, *args,
                timeout=timeout or self._default_timeout,
                retries=retries if retries is not None else self._default_retries,
                **kwargs,
            )
        )
        self._tasks.append(task)

    async def _run_with_control(
        self,
        name: str,
        coro_func: Callable[..., Coroutine],
        *args: Any,
        timeout: float,
        retries: int,
        **kwargs: Any,
    ) -> Any:
        """带并发控制、超时和重试的任务执行"""
        async with self._semaphore:
            last_error: Exception | None = None

            for attempt in range(1, retries + 1):
                try:
                    async with asyncio.timeout(timeout):
                        result = await coro_func(*args, **kwargs)
                        self._results[name] = result
                        logger.info(f"任务 [{name}] 成功 (第 {attempt} 次)")
                        return result

                except asyncio.TimeoutError:
                    last_error = TimeoutError(
                        f"任务 [{name}] 第 {attempt} 次超时 ({timeout}s)"
                    )
                    logger.warning(str(last_error))

                except Exception as e:
                    last_error = e
                    logger.warning(f"任务 [{name}] 第 {attempt} 次失败: {e}")

            self._errors[name] = last_error
            raise last_error  # type: ignore

    async def wait_all(self) -> tuple[dict, dict]:
        """等待所有任务完成,返回 (结果, 错误)"""
        if not self._tasks:
            return self._results, self._errors

        results = await asyncio.gather(*self._tasks, return_exceptions=True)
        return self._results, self._errors

    async def graceful_shutdown(self, timeout: float = 10.0) -> None:
        """优雅关闭:取消所有未完成任务"""
        pending = [t for t in self._tasks if not t.done()]
        if not pending:
            return

        logger.info(f"正在取消 {len(pending)} 个未完成任务...")
        for task in pending:
            task.cancel()

        try:
            async with asyncio.timeout(timeout):
                await asyncio.gather(*pending, return_exceptions=True)
        except asyncio.TimeoutError:
            logger.warning("部分任务未能在超时内完成取消")

        logger.info("优雅关闭完成")


# 使用示例
async def fetch_api(url: str) -> str:
    """模拟 API 请求"""
    await asyncio.sleep(1)
    if "fail" in url:
        raise ConnectionError(f"连接 {url} 失败")
    return f"数据来自 {url}"


async def main():
    manager = AsyncTaskManager(max_concurrency=3, default_timeout=5.0)

    # 提交多个任务
    urls = [
        "http://api1.com/data",
        "http://api2.com/data",
        "http://fail-api.com/data",  # 这个会失败
        "http://api3.com/data",
        "http://api4.com/data",
    ]

    for i, url in enumerate(urls):
        await manager.submit(f"task-{i}", fetch_api, url)

    # 等待所有任务完成
    results, errors = await manager.wait_all()

    print(f"\n成功: {len(results)} 个")
    print(f"失败: {len(errors)} 个")
    for name, error in errors.items():
        print(f"  {name}: {error}")

asyncio.run(main())

场景二:混合同步与异步代码

在实际项目中,经常需要将异步代码集成到同步框架中,或在异步代码中调用同步库:

python
import asyncio
import time
from typing import Any


class HybridService:
    """混合同步与异步的服务类

    场景:已有同步数据库客户端,需要集成到异步 Web 服务中
    """

    def __init__(self, db_url: str):
        self.db_url = db_url
        self._cache: dict[str, Any] = {}

    # ---- 同步方法(已有代码) ----

    def _sync_query(self, sql: str) -> list[dict]:
        """同步数据库查询(模拟已有代码)"""
        time.sleep(0.5)  # 模拟数据库 IO
        return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

    def _sync_cache_get(self, key: str) -> Any | None:
        """同步缓存读取"""
        return self._cache.get(key)

    def _sync_cache_set(self, key: str, value: Any) -> None:
        """同步缓存写入"""
        self._cache[key] = value

    # ---- 异步包装层 ----

    async def query(self, sql: str) -> list[dict]:
        """异步数据库查询(包装同步方法)"""
        # 先查缓存
        cache_key = f"query:{hash(sql)}"
        cached = await asyncio.to_thread(self._sync_cache_get, cache_key)
        if cached is not None:
            return cached

        # 缓存未命中,查询数据库
        result = await asyncio.to_thread(self._sync_query, sql)

        # 写入缓存
        await asyncio.to_thread(self._sync_cache_set, cache_key, result)
        return result

    async def batch_query(self, sqls: list[str]) -> list[list[dict]]:
        """批量异步查询"""
        tasks = [self.query(sql) for sql in sqls]
        return await asyncio.gather(*tasks)

    # ---- 纯异步方法 ----

    async def fetch_remote(self, url: str) -> str:
        """纯异步 HTTP 请求(使用异步库)"""
        await asyncio.sleep(0.3)  # 模拟 aiohttp 请求
        return f"远程数据来自 {url}"

    async def process(self, sql: str, api_url: str) -> dict:
        """混合处理:同时查询数据库和远程 API"""
        # 并行执行同步(线程化)和异步操作
        db_result, api_result = await asyncio.gather(
            self.query(sql),
            self.fetch_remote(api_url),
        )
        return {
            "database": db_result,
            "remote": api_result,
        }


async def main():
    service = HybridService("postgresql://localhost/mydb")

    # 单次查询
    result = await service.query("SELECT * FROM users")
    print(f"查询结果: {result}")

    # 批量查询
    results = await service.batch_query([
        "SELECT * FROM users",
        "SELECT * FROM orders",
    ])
    print(f"批量查询: {len(results)} 个结果集")

    # 混合处理
    combined = await service.process(
        "SELECT * FROM users",
        "http://api.example.com/data",
    )
    print(f"混合结果: {combined}")

asyncio.run(main())

常见陷阱

#陷阱错误示例后果正确做法
1在异步函数中调用同步阻塞 IOtime.sleep()requests.get()阻塞整个事件循环,所有协程停摆使用 asyncio.sleep()aiohttp,或 run_in_executor
2忘记 awaitresult = async_func() 而非 result = await async_func()协程不执行,得到协程对象而非结果始终 await 协程调用,或用 create_task 调度
3在 async 函数外使用 await直接在模块顶层 await coro()语法错误使用 asyncio.run() 包裹,或放在 async def
4create_task 后不保存引用asyncio.create_task(coro()) 不保存返回值Task 可能被垃圾回收导致"Task was destroyed"警告保存引用:task = asyncio.create_task(coro())
5吞掉 CancelledErrorexcept Exception 捕获了 CancelledError(3.8-)Task 无法被正确取消Python 3.9+ 中 CancelledError 继承 BaseException;始终重新抛出
6嵌套 asyncio.run在协程内调用 asyncio.run()RuntimeError: asyncio.run() cannot be called from a running event loop使用 create_taskgather
7gather 不处理异常await asyncio.gather(*tasks) 不设 return_exceptions任一 Task 异常导致整个 gather 失败设置 return_exceptions=True 或用 TaskGroup
8过度创建 Task循环中 create_task 不限制数量内存和调度开销激增使用 Semaphore 控制并发上限
9在 finally 中使用 await 但循环已关闭loop.close()await 协程RuntimeError: Event loop is closedclose() 前完成所有 await
10混用不同事件循环在一个线程创建循环,在另一个线程使用跨线程访问事件循环不安全使用 asyncio.run_coroutine_threadsafe() 跨线程提交

陷阱1详解:阻塞事件循环

这是最常见也最危险的陷阱:

python
import asyncio
import time

# 错误示范
async def bad_example():
    print("开始")
    time.sleep(3)       # 阻塞整个事件循环!
    await asyncio.sleep(1)
    print("结束")

# 正确做法
async def good_example():
    print("开始")
    await asyncio.sleep(3)  # 非阻塞等待
    await asyncio.sleep(1)
    print("结束")

# 如果必须调用同步阻塞函数
async def with_executor():
    print("开始")
    await asyncio.to_thread(time.sleep, 3)  # 在线程中执行
    await asyncio.sleep(1)
    print("结束")

陷阱4详解:Task 引用丢失

python
import asyncio

async def important_work():
    await asyncio.sleep(1)
    return "重要结果"

async def bad():
    # Task 没有保存引用,可能被 GC 回收
    asyncio.create_task(important_work())
    await asyncio.sleep(2)
    # 可能看到 "Task was destroyed but it is pending!" 警告

async def good():
    task = asyncio.create_task(important_work())
    # 方式1: 等待完成
    result = await task
    # 方式2: 保存到集合中
    # background_tasks = set()
    # task = asyncio.create_task(important_work())
    # background_tasks.add(task)
    # task.add_done_callback(background_tasks.discard)

最佳实践速查表

场景推荐做法避免做法
启动异步程序asyncio.run(main())手动创建循环(除非有特殊需求)
创建并发任务asyncio.create_task()TaskGroupasyncio.ensure_future()
等待多个结果asyncio.gather()手动逐个 await
按完成顺序处理asyncio.wait(FIRST_COMPLETED)gather + 手动排序
超时控制asyncio.timeout(3.11+)或 wait_for手动计算时间差
调用同步阻塞函数asyncio.to_thread()直接在协程中调用
CPU 密集型任务ProcessPoolExecutor + run_in_executor在协程中直接计算
并发控制asyncio.Semaphore无限制创建 Task
异常处理return_exceptions=TrueTaskGroup忽略 Task 异常
取消任务task.cancel() + await task只调用 cancel() 不等待
跨线程提交协程run_coroutine_threadsafe()直接从其他线程调用协程
资源清理try/finallyasync with依赖 GC 清理
调试asyncio.run(main(), debug=True)无调试手段

术语表

术语英文定义
事件循环Event Loop调度和执行协程、处理 IO 回调的核心引擎,asyncio 的运行时基础
协程Coroutineasync def 定义的函数,可在 await 处暂停和恢复执行
TaskTask事件循环中对协程的包装,是可调度的执行单元
FutureFuture表示异步操作最终结果的占位对象,Task 是 Future 的子类
事件循环策略Event Loop Policy控制事件循环创建和管理的策略对象,可自定义
IO 多路复用IO Multiplexing操作系统提供的机制(epoll/kqueue/IOCP),同时监控多个文件描述符的就绪状态
SelectorSelectorasyncio 对 IO 多路复用的抽象层,提供统一的 select() 接口
ProactorProactor基于 IOCP 的异步模式,由操作系统完成 IO 后通知应用
TickTick事件循环的一次完整迭代,包含回调处理、IO 检查和定时器处理
结构化并发Structured ConcurrencyTaskGroup 代表的并发模式,保证所有子任务在退出时已完成
ExecutorExecutor线程池或进程池,用于执行同步阻塞函数,避免阻塞事件循环
取消点Cancellation Point协程中检查取消状态的时机,通常是 await 表达式
异步上下文管理器Async Context Manager实现 __aenter____aexit__ 的对象,支持 async with
异步生成器Async Generator使用 yieldasync def 函数,支持 async for 遍历
事件循环迭代Loop Iteration事件循环执行一次完整的调度周期,处理所有就绪事件
回调Callback注册到事件循环的函数,在特定条件(IO 就绪、定时器到期)时被调用
协程调度Coroutine Scheduling事件循环决定何时恢复挂起的协程执行的过程
并发原语Concurrency Primitiveasyncio 提供的同步工具:Lock、Event、Condition、Semaphore、Queue

延伸阅读

站内链接

外部链接

版本差异(asyncio → Python 3.14)

特性本文编写时Python 3.14
事件循环入口loop.run_until_complete()推荐 asyncio.run()(3.7+);3.10+ 禁止在没有运行循环时调用 get_event_loop() 创建
任务组create_task() 手动管理3.11+ 推荐 asyncio.TaskGroup 结构化并发:自动取消、聚合异常(ExceptionGroup
超时wait_for()3.11+ 推荐 asyncio.timeout() 上下文管理器
线程混合run_in_executor()3.9+ asyncio.to_thread() 更简洁
内省3.14 新增 asyncio 内省能力(任务/未来状态查询)
取消语义3.9+ CancelledError 继承 BaseExceptionexcept Exception 捕获不到

本文讲解的协程/事件循环核心机制在 3.14 中成立;新代码建议使用 asyncio.run() + TaskGroup + timeout() 结构化编程。