第 14 章 · 协程与 asyncio
本章目标:理解 协程与 async/await 语义;使用 asyncio 运行与调度协程;掌握 create_task / gather / wait_for;对比 线程 / 进程 / 协程 选型;能编写 高并发 I/O 脚本(并发 HTTP、异步读写);了解 aiohttp 与同步 requests 的差异。
学时建议:4~5 小时(含 2 小时 async 实验)
前置:ch12 多线程;ch13 多进程;ch11 requests;ch05 函数。
14.1 协程是什么
协程(coroutine) 是可暂停、可恢复的函数;遇到 I/O 等待时让出控制权,事件循环去跑别的协程。
线程:OS 调度,抢占式,有栈
协程:事件循环调度,协作式,单线程内切换
| 对比 | 线程 (ch12) | 协程 (本章) |
|---|---|---|
| 调度 | OS | asyncio 事件循环 |
| 内存 | 每线程 ~MB 栈 | 轻量,大量连接 |
| 适用 | 一般 I/O | 海量 I/O(爬虫、网关) |
| CPU 密集 | 受 GIL 限 | 仍受 GIL,应放进程池 |
shop-demo 场景:同时检查 200 个商品 URL 是否 200 OK——协程比 200 线程更省资源。
14.2 第一个 async 程序
import asyncio
async def fetch_name(sku: str) -> str:
await asyncio.sleep(0.5) # 模拟 I/O,不阻塞整个进程
return f"product-{sku}"
async def main() -> None:
result = await fetch_name("BK-001")
print(result)
if __name__ == "__main__":
asyncio.run(main())
| 关键字 | 含义 |
|---|---|
async def | 定义协程函数 |
await | 等待可等待对象,期间可切换 |
asyncio.run(main()) | 创建事件循环并运行顶层协程 |
规则:await只能在async def内使用;协程必须被 await 或交给事件循环,否则只得到 coroutine 对象。
14.3 并发:create_task 与 gather
串行(慢):
async def main():
a = await fetch_name("A")
b = await fetch_name("B")
并发(快):
async def main() -> None:
t1 = asyncio.create_task(fetch_name("A"))
t2 = asyncio.create_task(fetch_name("B"))
print(await t1, await t2)
# 或一次性等待多个
async def main_gather() -> None:
results = await asyncio.gather(
fetch_name("A"),
fetch_name("B"),
fetch_name("C"),
)
print(results)
| API | 说明 |
|---|---|
create_task(coro) | 排入循环,立即返回 Task |
asyncio.gather(*aws) | 并发等待,结果顺序与传入一致 |
return_exceptions=True | 某个失败不拖垮全部 |
14.4 超时与取消:wait_for
async def slow():
await asyncio.sleep(10)
async def main():
try:
await asyncio.wait_for(slow(), timeout=2.0)
except asyncio.TimeoutError:
print("timeout")
生产环境对外部 HTTP 必须设 timeout(与 ch11 requests 同理)。
14.5 实战:并发探测 URL
使用标准库 asyncio + 线程池跑阻塞 I/O(无需额外安装):
import asyncio
import urllib.request
URLS = [
"https://httpbin.org/get",
"https://httpbin.org/delay/1",
"https://httpbin.org/status/404",
]
def blocking_fetch(url: str) -> tuple[str, int]: