PyDev Notes

深入理解 Python 异步编程:从 yield 到 async/await

Python 异步编程 asyncio 协程

一、为什么需要异步编程

在传统的同步编程模型中,当程序执行 I/O 操作(如网络请求、文件读写、数据库查询)时,线程会被阻塞,直到操作完成。对于高并发场景,这种模型效率极低——一个 Web 服务器如果为每个请求分配一个线程,当并发量达到数千时,线程切换的开销和内存占用就会成为瓶颈。

异步编程的核心思想是:当遇到 I/O 等待时,主动让出控制权,让其他任务继续执行,等 I/O 完成后再恢复执行。这样,单线程就能处理大量并发 I/O 操作。

二、从生成器到协程

Python 的异步编程并非凭空出现,它经历了从生成器(Generator)到协程(Coroutine)的演进。在 Python 2.2 时代,yield 关键字的引入让函数可以暂停执行并产出值:

def simple_generator():
    yield 1
    yield 2
    yield 3

for val in simple_generator():
    print(val)  # 1, 2, 3

聪明的开发者很快发现,yield 不仅能产出值,还能接收值(通过 send() 方法),这意味着生成器可以暂停和恢复执行——这正是协程的核心特征。Python 社区用 yield 实现了最早的异步协程框架,如 Tornado 和早期的 asyncio。

三、async/await 语法

Python 3.5 正式引入了 asyncawait 关键字,让协程有了原生的语法支持:

import asyncio
import aiohttp

async def fetch_url(url: str) -> str:
    """异步获取 URL 内容"""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    urls = [
        "https://httpbin.org/get",
        "https://httpbin.org/ip",
        "https://httpbin.org/headers",
    ]
    # 并发请求所有 URL
    tasks = [fetch_url(url) for url in urls]
    results = await asyncio.gather(*tasks)
    for r in results:
        print(r[:100])

asyncio.run(main())

关键要点:

四、事件循环机制

asyncio 的核心是事件循环(Event Loop),它负责调度和执行协程。理解事件循环的工作原理,是写出正确异步代码的基础:

import asyncio

async def task(name: str, seconds: int):
    print(f"任务 {name} 开始,预计耗时 {seconds}s")
    await asyncio.sleep(seconds)
    print(f"任务 {name} 完成")
    return f"{name}-result"

async def main():
    # 三个任务并发执行,总耗时约 3 秒而非 6 秒
    results = await asyncio.gather(
        task("A", 3),
        task("B", 2),
        task("C", 1),
    )
    print(f"结果: {results}")

asyncio.run(main())
# 输出顺序: A开始, B开始, C开始, C完成, B完成, A完成

五、常见陷阱与最佳实践

  1. 不要在异步代码中调用同步阻塞函数:如 time.sleep()requests.get(),它们会阻塞整个事件循环。应替换为 asyncio.sleep()aiohttp 等异步版本。
  2. CPU 密集型任务用线程池:asyncio 适合 I/O 密集型,CPU 密集型任务应使用 asyncio.run_in_executor() 放到线程池中执行。
  3. 注意协程的创建与调度:仅调用 async def 函数不会执行它,必须 await 或用 asyncio.create_task() 调度。
  4. 异常处理asyncio.gather() 默认在某个任务异常时会立即抛出,使用 return_exceptions=True 可以收集所有结果。
# CPU 密集型任务正确用法
async def cpu_bound_work():
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, heavy_computation)
    return result

def heavy_computation():
    # 耗时的 CPU 计算
    return sum(i * i for i in range(10_000_000))

六、总结

Python 异步编程从 yield 的巧妙运用,发展到现在成熟的 async/await 语法和 asyncio 生态,已经成为构建高性能 I/O 密集型应用的核心工具。掌握事件循环、协程调度、并发模式这些核心概念,才能写出正确且高效的异步代码。

下一步,可以深入学习 FastAPI 框架,它在 asyncio 之上构建了现代化的异步 Web 开发体验,结合类型注解和自动文档生成,是当前 Python Web 开发最流行的选择之一。