Python 异步编程:async、await 与事件循环

# Python 异步编程:async、await 与事件循环

本篇目标

理解协程、Task、事件循环和并发控制,能判断异步适合什么问题,并避免阻塞事件循环。

理解事件循环创建并发任务设置超时取消区分并发与并行

# 先记住一句话

asyncio 通过单线程事件循环协作调度大量 I/O 任务;协程只有在执行到可等待操作时主动让出控制权。

Task A 执行 → 等待网络 I/O ─┐
                            ↓
Task B 执行 → 等待数据库 ───→ 事件循环继续调度可运行任务
                            ↓
某个 I/O 完成 → 恢复对应 Task
1
2
3
4
5

异步并不会让 CPU 计算自动变快。它主要减少网络、磁盘、数据库等等待时间造成的线程闲置。

# 协程怎样运行

import asyncio

# async def 定义协程函数;调用它会得到协程对象,需要由 await 或事件循环执行。
async def fetch_note(note_id: str) -> str:
    # sleep 模拟非阻塞网络等待;真实项目会调用异步 HTTP 或数据库客户端。
    # await 会暂停当前协程,把执行机会交还事件循环,等待完成后再从这里继续。
    await asyncio.sleep(0.1)
    return f"note:{note_id}"

async def main() -> None:
    # TaskGroup 会并发运行任务;任一任务失败时会取消同组未完成任务。
    async with asyncio.TaskGroup() as group:
        first = group.create_task(fetch_note("py"))
        second = group.create_task(fetch_note("go"))

    # 离开 TaskGroup 时任务都已结束,可以安全读取结果。
    print(first.result(), second.result())

# asyncio.run 创建事件循环、执行入口协程并负责收尾。
if __name__ == "__main__":
    asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

调用 fetch_note("py") 只会创建协程对象,不会自动跑完。必须通过 await、TaskGroup 或其他调度方式把它交给事件循环。

# 超时、取消和并发上限

import asyncio
from collections.abc import Awaitable, Callable

# Callable[[str], Awaitable[str]] 表示 fetcher 接收字符串,并返回一个可等待的字符串结果。
async def run_limited(
    ids: list[str],
    fetcher: Callable[[str], Awaitable[str]],
) -> list[str]:
    # 信号量限制同时在途请求数,避免压垮下游服务。
    semaphore = asyncio.Semaphore(5)

    async def one(note_id: str) -> str:
        async with semaphore:
            # 单次调用超过两秒就取消等待,并向上抛出 TimeoutError。
            async with asyncio.timeout(2):
                return await fetcher(note_id)

    # 生成器逐个创建协程;前面的 * 将它们展开成 gather 的多个参数。
    # gather 并发等待全部协程,并按照传入顺序返回结果。
    return await asyncio.gather(*(one(note_id) for note_id in ids))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

取消是协作式的。协程要执行到 await 才有机会接收取消。捕获 CancelledError 后通常应完成必要清理并继续抛出,否则上层可能误判任务仍在正常运行。

# 最常见的错误:阻塞事件循环

import asyncio
from pathlib import Path

# Path 是面向对象的文件路径;read_text() 仍是同步阻塞 I/O。
def read_large_file(path: Path) -> str:
    """同步阻塞函数,假设暂时无法替换为异步实现。"""
    return path.read_text(encoding="utf-8")

async def load_without_blocking(path: Path) -> str:
    # 把同步 I/O 放到工作线程,避免卡住事件循环中的其他请求。
    return await asyncio.to_thread(read_large_file, path)
1
2
3
4
5
6
7
8
9
10
11

工作线程不是无限资源。大量 CPU 密集任务应考虑进程池、任务队列或原生扩展,而不是全部塞进 to_thread。

# 怎样选择执行模型

场景 常见选择 原因
少量简单同步任务 普通同步代码 最容易理解和调试
大量网络 I/O asyncio 单线程即可管理许多等待任务
阻塞 I/O 库 线程池 不阻塞事件循环,迁移成本低
纯 Python CPU 密集计算 多进程或外部计算服务 绕开传统 GIL 的执行限制

# 高频面试题与回答

1. 并发和并行有什么区别?参考答案

并发是多个任务在一段时间内交替推进,并行是多个任务在同一时刻真正执行。asyncio 通常在一个线程中实现并发,适合 I/O 等待;CPU 密集计算要根据实现选择多进程或其他并行方案。

2. `async def` 中为什么仍可能阻塞?参考答案

async def 只创建协程语义,不会把里面的同步操作自动变成非阻塞。若执行同步 HTTP、文件读取或长时间 CPU 计算,事件循环仍会被占住,其他协程无法及时运行。

3. 协程、Task 和 Future 有什么区别?参考答案

调用异步函数得到协程对象,它描述一段可暂停的工作;Task 把协程提交给事件循环调度,并保存完成、结果或异常状态;Future 是更底层的可等待结果占位对象。业务代码通常直接 await 协程,只有需要并发推进和统一管理时才显式创建 Task。

4. asyncio 取消任务是立即强制终止吗?参考答案

不是。取消是协作式的,事件循环会在任务下一次获得执行机会时抛出取消异常,任务必须到达 await 等可暂停位置才能响应。清理资源应放进 finally 或上下文管理器,并且不要无意吞掉取消信号,否则超时和服务关闭都会失效。

# 接下来学什么

下一篇学习 线程、多进程与 asyncio,比较不同执行模型适合的任务类型、限制和工程边界。

# 参考资料

上次更新时间: 2026年09月10日 00:03:52