第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 如何让控制权在协程和事件循环之间切换。
图中左边解释了协程内部发生了什么,右边展示了事件循环如何穿插调度。核心就是:await 是协程的“暂停点”,事件循环是“调度员”。记住这个模型,你会觉得异步编程其实也没那么玄乎。
一句话总结:
async def 定义协程,await 让出控制权。事件循环拿到控制权后可以运行其他协程,等 IO 完成了再回来继续。这就是异步并发的本质。