>>>PyPathPython 学习站
首页›进阶篇›kp-031
进阶篇 · Advanced lv.3 进阶 kp-031

异步编程(asyncio)

前置知识:kp-029、kp-013

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-032

7. 直观类比

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. 常见误区

  1. 协程忘了 await / 忘了 create_task —— fetch(url) 只创建了协程对象,从未执行;RuntimeWarning: coroutine was never awaited 就是这个病。
  2. 在协程里用同步库 —— requests、time.sleep、同步 DB 驱动都会卡死整个循环;找异步版或 to_thread。
  3. 以为 async 提速 CPU 计算 —— 它只优化 IO 等待;重计算照样单线程慢。
  4. 裸 asyncio.gather 不处理异常 —— gather 默认首个异常向上抛但其他任务继续跑完;需要"失败即取消"用 TaskGroup。

10. 自测题

  1. async def f(): ...; f() 得到什么?如何真正执行它?
  2. 为什么 time.sleep(1) 在协程里是灾难?正确写法?
  3. TaskGroup 与 gather 的行为差异?
参考答案
  1. 协程对象(未执行);await f()、asyncio.create_task(f()) 或 asyncio.run(f()) 驱动。
  2. 同步阻塞冻住整个事件循环,所有任务停摆;await asyncio.sleep(1)。
  3. TaskGroup 是结构化并发:组内任一任务失败即取消全组并聚合异常,退出时保证全部结束;gather 只并发等待,异常语义较宽松。

11. 与其他知识点的关系

  • kp-042 Web 生态:ASGI 与异步框架。
  • kp-030 线程池:混合负载用 to_thread 桥接。
  • kp-013 生成器:async def 是 yield 概念的直系后代。

12. 延伸阅读

  • asyncio 官方文档:https://docs.python.org/zh-cn/3/library/asyncio.html
  • PEP 492(async/await)