Python 异步编程:从生成器到 asyncio

2026-08-31 44 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:6 分钟

当程序的大部分时间都在等待网络、磁盘或其他外部服务时,单纯增加 CPU 或线程并不能解决所有问题。Python 的异步编程通过生成器、asyncawaitasyncio,让程序在等待 I/O 时处理其他任务,从而提高并发处理能力。

从生成器理解“暂停与恢复”

生成器使用 yield 暂停函数执行,并在下次迭代时恢复。它本身不等同于异步编程,但提供了一个重要思路:函数可以暂时交出控制权,稍后再继续执行。

def numbers():
    yield 1
    yield 2
    yield 3

for number in numbers():
    print(number)

在异步程序中,任务也需要暂停和恢复。区别在于,事件循环会在任务等待 I/O 时调度其他任务运行。

asyncawait 做了什么

使用 async def 定义异步函数。调用它时不会立即执行函数体,而是返回一个 coroutine 对象。只有通过 await 或事件循环运行它,这个协程才会真正执行。

await 表示当前任务将在这里等待一个异步结果。在等待期间,事件循环可以执行其他已经准备好的任务。

需要注意,await 只能等待可等待对象,例如协程、任务或某些异步库返回的对象。把同步阻塞函数直接放进 async def,并不会自动变成异步操作。

用 asyncio 并发执行 I/O 任务

下面的示例使用 asyncio.sleep() 模拟网络请求或其他慢速 I/O。asyncio.gather() 会并发调度多个协程,并等待它们全部完成。

将下面内容保存为 async_demo.py,使用 Python 3.9 或更高版本运行:

import asyncio
import time


async def fetch(name: str, delay: float) -> str:
    print(f"开始请求 {name}")
    await asyncio.sleep(delay)  # 模拟网络 I/O
    print(f"完成请求 {name}")
    return f"{name}: ok"


async def main() -> None:
    start = time.perf_counter()

    results = await asyncio.gather(
        fetch("users", 1.0),
        fetch("orders", 1.5),
        fetch("inventory", 0.8),
    )

    elapsed = time.perf_counter() - start
    print("结果:", results)
    print(f"总耗时: {elapsed:.2f} 秒")


if __name__ == "__main__":
    asyncio.run(main())

这三个任务的总耗时接近最长的单个任务,而不是三个延迟之和。真实项目中,可以把 asyncio.sleep() 换成支持异步的 HTTP 客户端、数据库驱动或文件操作库。

如果调用的是普通阻塞函数,例如同步 HTTP 客户端,可以用线程池暂时隔离阻塞调用:

import asyncio
import time


def blocking_work() -> str:
    time.sleep(2)
    return "完成同步阻塞操作"


async def main() -> None:
    result = await asyncio.to_thread(blocking_work)
    print(result)


if __name__ == "__main__":
    asyncio.run(main())

这不是把同步库变成真正的异步库,而是避免它直接阻塞事件循环。长期运行的服务仍应优先选择原生支持异步的库。

并发不等于无限制地发请求

并发任务过多会耗尽连接池、触发服务端限流,甚至让本地内存和文件描述符达到上限。可以用 asyncio.Semaphore 限制同时运行的任务数量:

import asyncio


async def fetch(item: int, limit: asyncio.Semaphore) -> str:
    async with limit:
        print(f"处理任务 {item}")
        await asyncio.sleep(0.5)
        return f"任务 {item} 完成"


async def main() -> None:
    limit = asyncio.Semaphore(3)
    results = await asyncio.gather(
        *(fetch(item, limit) for item in range(10))
    )
    print("\n".join(results))


if __name__ == "__main__":
    asyncio.run(main())

这里最多同时执行三个任务。生产环境还需要根据接口限制设置超时、重试、取消处理和错误记录。

采用 asyncio 前的检查清单

  • 确认瓶颈主要来自网络、数据库或其他 I/O,而不是 CPU 密集计算。
  • 检查依赖库是否提供异步 API;同步调用会阻塞事件循环。
  • 使用 asyncio.gather() 或任务组组织并发任务,同时处理异常和取消。
  • 为并发数量设置上限,避免连接池和下游服务承压。
  • 为每次 I/O 设置超时,并记录任务失败、重试和耗时。
  • CPU 密集型工作通常应考虑进程池、独立任务队列或其他并行方案。

异步编程的核心不是到处添加 async,而是让任务在等待期间把执行权交回事件循环。理解生成器的暂停机制,再掌握 asyncawaitasyncio 的调度方式,就能更准确地判断哪些代码适合并发,哪些代码只会增加复杂度。


相关推荐