第4章 async/await 语法详解

协程这玩意儿,说白了就是能暂停又能恢复的函数。Python 用 async def 定义它,用 await 挂起它。我刚开始学的时候总觉得跟生成器很像,后来踩了几个坑才明白——差远了。

4.1 定义协程:async def

定义一个协程,就在 def 前面加个 async。就这么简单。但调用它的时候不会立即执行,而是返回一个协程对象。你想想看,这跟普通函数完全不一样。

async def fetch_data(url):
    # 模拟网络请求
    return f"data from {url}"

# 调用后返回协程对象,不会执行里面的代码
coro = fetch_data("https://api.example.com")
print(coro)  # <coroutine object fetch_data at 0x...>
提示:很多新手会直接在 async def 函数里写 return 值,然后期待它像普通函数一样返回。实际上,你需要用 await 或者 asyncio.run() 才能拿到结果。我见过好几次线上 bug 就是因为忘了跑协程。

4.2 await 表达式:挂起与恢复

await 后面跟一个可等待对象(awaitable)。协程、Future、Task 都算。遇到 await 时,当前协程会暂停,把控制权交还给事件循环。等被等待的对象完成了,再回来继续往下走。

import asyncio

async def slow_op():
    await asyncio.sleep(1)      # 暂停1秒,不阻塞线程
    return "done"

async def main():
    result = await slow_op()    # 等待 slow_op 完成
    print(result)               # 输出 "done"

asyncio.run(main())
警告:await 只能在 async def 内部使用。在普通函数里写 await 会直接报 SyntaxError。我曾经在视图函数里不小心把 await 写到了同步 def 中,结果 Django 直接崩溃,排查了好久。

4.3 协程 vs 普通函数:一张表看透

特性 普通函数 协程
定义 def async def
调用结果 立即执行并返回结果 返回协程对象,不执行
暂停/恢复 不支持 支持(通过 await)
返回值获取 直接赋值 需要 await 或 asyncio.run()
异常处理 try/except 正常使用 一样可以用 try/except,但要注意协程内部异常也会在 await 时抛出

4.4 实战:用 async/await 写一个并发下载器

光说理论不够。我给你看一段我在项目中真的用过的代码。那次需要从三个 API 同时拉数据,串行要 3 秒,改成协程只花 1 秒。

import asyncio
import time

async def download(url):
    print(f"开始下载 {url}")
    await asyncio.sleep(1)  # 模拟 IO
    print(f"完成下载 {url}")
    return f"内容来自 {url}"

async def main():
    tasks = [
        download("https://api.a.com"),
        download("https://api.b.com"),
        download("https://api.c.com"),
    ]
    results = await asyncio.gather(*tasks)
    return results

start = time.time()
results = asyncio.run(main())
print(f"耗时: {time.time() - start:.2f}秒")
关键点:asyncio.gather 并发运行多个协程,并且会等待所有协程完成。如果其中一个抛异常,默认会冒泡到 await gather 的地方。我习惯给 gather 加上 return_exceptions=True 来收集所有异常,避免一个出错全部失败。

4.5 避坑指南(我的血泪史)

  • 别在协程里用 time.sleep()——它会阻塞整个线程,所有协程都得等着。用 asyncio.sleep() 才行。我曾经在异步爬虫里误用了 time.sleep(0.5),结果并发变成串行,速度没提升,反而比同步还慢。
  • await 只能等可等待对象——记住只有协程、Task、Future 能跟 await。普通函数、整数、字符串都不行。你想想看,如果在一个整数前面加 await,Python 会直接告诉你 TypeError: object int can't be used in 'await' expression。
  • 协程里不要写 CPU 密集型操作——协程是协作式调度,如果你在协程里做大量计算,事件循环会被卡住,其他协程就饿死了。遇到 CPU 密集任务,扔给线程池或者进程池处理。

4.6 一张图搞懂 async/await 执行流程

下面这张 SVG 流程图,展示了一个简单协程从定义到执行完毕的完整过程。注意箭头上的 await 如何让控制权在协程和事件循环之间切换。

async/await 执行流程 async def fetch() coro = fetch() asyncio.run(coro) 事件循环调度 await asyncio.sleep(1) IO 完成,恢复 协程内部执行过程 1. 执行 await 之前的代码 2. 遇到 await 暂停,返回事件循环 3. 等待对象就绪后恢复执行 事件循环职责 - 管理所有待执行协程 - 轮询等待的 IO 是否完成 - 完成时恢复对应协程

图中左边解释了协程内部发生了什么,右边展示了事件循环如何穿插调度。核心就是:await 是协程的“暂停点”,事件循环是“调度员”。记住这个模型,你会觉得异步编程其实也没那么玄乎。

一句话总结:async def 定义协程,await 让出控制权。事件循环拿到控制权后可以运行其他协程,等 IO 完成了再回来继续。这就是异步并发的本质。