异步编程 asyncio
asyncio 是 Python 高并发 IO 的标准方案,也是 FastAPI 的底层。面试主线:事件循环怎么转、async/await 是什么、怎么控制并发。
协程与事件循环
事件循环: IO 等待时切换协程
- 协程:
async def定义的函数,调用不执行,返回协程对象 - 事件循环:单线程调度器,管理所有协程;协程
await时让出控制权,IO 就绪后恢复 await:挂起当前协程直到结果就绪。await 的对象必须是可等待的(协程、Task、Future)
async def fetch(url):
data = await http_get(url) # 挂起, 循环去跑别的协程
return data
async def main():
tasks = [asyncio.create_task(fetch(u)) for u in urls]
results = await asyncio.gather(*tasks) # 并发执行Task 与并发
| 工具 | 作用 |
|---|---|
asyncio.create_task |
把协程包装成 Task 立即调度,不等待结果 |
asyncio.gather |
并发运行多个协程,等全部完成 |
asyncio.wait_for |
超时控制 |
asyncio.Semaphore |
限并发数(信号量) |
asyncio.Lock |
临界区保护 |
sem = asyncio.Semaphore(10) # 最多 10 个并发
async def limited(url):
async with sem:
return await fetch(url)信号量是高频考点:批量任务要限并发(防止打爆下游),async with sem 是标准写法。
与线程的对比
| 维度 | 线程 | 协程 |
|---|---|---|
| 调度 | 操作系统抢占式 | 事件循环协作式 |
| 切换代价 | 内核切换,重 | 函数调用级,轻 |
| 并发规模 | 千级 | 十万级(内存占用小) |
| 数据竞争 | 有(要锁) | 单线程内无竞争 |
| 阻塞调用 | 不阻塞其他线程 | 阻塞整个事件循环 |
协程的代价:任何阻塞调用(time.sleep、同步 IO、CPU 计算)都会卡住整个事件循环,所有协程停摆。解决办法:
- 同步库调用放
asyncio.to_thread/run_in_executor(丢线程池) - 用异步库(httpx.AsyncClient、asyncpg)代替同步库
- 重计算单独进程
面试追问
- async/await 怎么工作? async def 定义协程,await 挂起让出控制权,事件循环调度,IO 就绪恢复。协作式
- create_task 和 gather 的区别? create_task 创建任务立即调度;gather 并发等待全部完成。gather 内部也建 Task
- 信号量干什么? 限并发数。async with sem 保护,防止一次性打爆下游
- 协程里能调 time.sleep 吗? 不能,会阻塞整个事件循环。用 await asyncio.sleep,同步阻塞调用丢 to_thread
- 协程和线程怎么选? 高并发 IO 用协程(十万级),需要真并行 CPU 用进程,简单 IO 任务线程也够