进阶篇 · Advanced
lv.3 进阶
kp-031
异步编程(asyncio)
1. 一句话定义
asyncio 用单线程事件循环跑"协作式多任务":async def 定义协程,await 处主动让出控制权——用同步的写法获得万级 IO 并发。
2. 为什么重要
网络服务(爬虫、API 网关、Bot)的高并发标准解法。FastAPI、aiohttp、discord.py 全部构建其上。理解 await 的"让出点"心智,是从同步思维到异步思维的一跃。
3. 前置知识
kp-013(生成器——协程的历史前身)、kp-029(并发选型)。
4. 核心概念
python
import asyncio, httpx # 异步 HTTP 客户端示例
async def fetch(client, url):
r = await client.get(url, timeout=5) # await:让出点,循环去跑别人
return r.status_code
async def main():
async with httpx.AsyncClient() as client:
tasks = [asyncio.create_task(fetch(client, u)) for u in urls]
results = await asyncio.gather(*tasks) # 并发跑完再聚合
return results
asyncio.run(main()) # 程序入口:创建事件循环并运行 main- 协程函数:
async def;调用它得协程对象(不执行),await才驱动。 - await 只能 await 可等待对象:协程、Task、Future。
- create_task:把协程交给循环"现在就开始跑"。
- gather:并发等待一批;
TaskGroup(3.11+)是更安全的替代(自动取消、结构化并发)。
5. 原理与机制
text
事件循环单线程调度:
task A ──await 网络响应──► 挂起,注册回调
task B ──await 睡 1s──────► 挂起
循环:谁的就绪事件到了,恢复谁(协作式,无抢占)
关键不变量:await 之间的代码是原子的(不会被同循环内其他协程插入)
代价:一个阻塞调用卡住整个循环因此铁律是:异步世界里绝不允许同步阻塞调用(time.sleep、同步 requests、重 CPU 计算都会冻住全部任务)。无法避免时用 await asyncio.to_thread(blocking_fn) 把阻塞丢进线程池。
超时与并发控制:
python
async with asyncio.timeout(5): # 3.11+
await do()
await asyncio.Semaphore(50) # 限流:包住并发段6. 关键事实(模型/图示)
text
同步线程模型:并发 = 多线程 + 操作系统抢占切换(真并行潜力,受 GIL 限)
异步协程模型:并发 = 单线程 + await 让出切换(无锁竞争、开销极小、无并行)
IO 密集 + 大量连接 → asyncio 赢;CPU 密集 → 都不行,去 kp-0327. 直观类比
asyncio 是一个只带一部手机的客服:顾客 A 打来,问到要查数据(await),就把电话挂起去接顾客 B;数据回了再切回 A。单线程没有抢话筒问题(无锁),但任何一通电话里去泡咖啡(同步阻塞),所有人都在等这位客服。
8. 实例与案例
python
# 结构化并发(3.11+ TaskGroup):一个失败全组取消
async def crawl(urls):
async with httpx.AsyncClient() as client:
async with asyncio.TaskGroup() as tg:
results = [tg.create_task(fetch(client, u)) for u in urls]
return [t.result() for t in results]9. 常见误区
- 协程忘了 await / 忘了 create_task ——
fetch(url)只创建了协程对象,从未执行;RuntimeWarning: coroutine was never awaited就是这个病。 - 在协程里用同步库 —— requests、
time.sleep、同步 DB 驱动都会卡死整个循环;找异步版或to_thread。 - 以为 async 提速 CPU 计算 —— 它只优化 IO 等待;重计算照样单线程慢。
- 裸
asyncio.gather不处理异常 —— gather 默认首个异常向上抛但其他任务继续跑完;需要"失败即取消"用 TaskGroup。
10. 自测题
async def f(): ...; f()得到什么?如何真正执行它?- 为什么
time.sleep(1)在协程里是灾难?正确写法? - TaskGroup 与 gather 的行为差异?
参考答案
- 协程对象(未执行);
await f()、asyncio.create_task(f())或asyncio.run(f())驱动。 - 同步阻塞冻住整个事件循环,所有任务停摆;
await asyncio.sleep(1)。 - TaskGroup 是结构化并发:组内任一任务失败即取消全组并聚合异常,退出时保证全部结束;gather 只并发等待,异常语义较宽松。
11. 与其他知识点的关系
12. 延伸阅读
- asyncio 官方文档:https://docs.python.org/zh-cn/3/library/asyncio.html
- PEP 492(async/await)