Python 协程库如何使用以及有哪些使用场景

协程本质:它不是多线程并行,而是一个线程里多个"任务在等 IO 时互相让出 CPU"的合作式并发async def / await 就是在告诉事件循环:"我现在在等网络/DB/睡眠,把控制权拿走去跑别的协程吧,等我这个 IO 好了再回来接着跑。"Python 官方 asyncio 就是用 async/await 写并发代码、做异步 IO/网络/结构化网络代码的库,官方也明确说它很适合 IO-bound 和高层次结构化网络代码


一、最小心智模型:协程 = 能暂停的函数

普通函数是"从头跑到尾,中间不让人插队"。协程是 async def 定义的,遇到 await xxx挂起自己、把控制权还给事件循环 ,等 xxx 这个可等待对象完成了,再回来拿结果继续往下走。

python 复制代码
import asyncio

async def fetch():
    print("start")
    await asyncio.sleep(1)   # 模拟 IO 等待:把自己挂起,事件循环可以跑别的
    print("end")
    return 42

async def main():
    r = await fetch()        # await:等 fetch 这个协程跑完拿结果
    print(r)

asyncio.run(main())          # 官方推荐的入口:创建事件循环、跑 main、关循环

这里有个新手必踩的点:fetch() 只是返回了一个协程对象,不跑 ;得 await fetch()asyncio.run()create_task() 才会真被调度。官方文档专门警告过:简单调用协程不会调度执行,只拿到 <coroutine object ...>

事件循环可以理解成一个"调度器/乐队指挥":它手里有一堆就绪/挂起的协程,一个协程 await 让出控制权,它就挑下一个能跑的继续跑;没人可跑就睡一会儿别空转。


二、asyncio 最常用的 10 个用法

1. asyncio.run(main()) ------ 程序入口

一个进程里一般别乱开一堆 loop,顶层入口用 asyncio.run(main()) 就够了。它创建/关闭事件循环,跑完 main() 结束。Jupyter 这类已经有运行 loop 的环境里可以直接 await main()asyncio.run 不要在已跑的 loop 里再调。

2. await ------ "等这个 IO 完再说"

python 复制代码
r = await asyncio.sleep(1)   # 等 1 秒,期间事件循环能跑别人
r = await async_http_get()    # 等 HTTP 响应
row = await conn.fetchrow(sql) # 等 DB

await 只能在 async def 里用,能接的是"可等待对象":协程、TaskFuture

3. create_task ------ "启动它,但别傻等,先干别的"

await A(); await B() 是串行的:A 完了才 B。create_task 是把协程变成"已经在 loop 里排队的任务",后面一起 await,就有并发了:

python 复制代码
async def main():
    t1 = asyncio.create_task(download("a"))
    t2 = asyncio.create_task(download("b"))
    r1 = await t1
    r2 = await t2

或者更干净:

python 复制代码
async with asyncio.TaskGroup() as tg:
    t1 = tg.create_task(download("a"))
    t2 = tg.create_task(download("b"))
# 退出 with 时所有任务完成或异常传播

官方文档说 create_task 用于把多个协程当 asyncio Task 并发跑;新版 TaskGroup 是更现代的"结构化并发"替代,异常/取消更好管理。

4. asyncio.gather ------ 一把等一堆任务

最常用在"我要并发调 100 个 URL / 100 个子任务,等全部完":

python 复制代码
urls = [...]
tasks = [fetch_url(u) for u in urls]
results = await asyncio.gather(*tasks)

好处是简单;坏处是老版本 gather 对异常/取消的语义没 TaskGroup 那么结构化。新代码想"一组子任务同生共死/出错就取消别人",优先 TaskGroup;想"我就要所有结果/异常收集回来",gather 仍然好用

5. asyncio.sleep ------ 永远别在协程里 time.sleep

python 复制代码
# 错:time.sleep(1) 会阻塞整个事件循环 1 秒,所有协程都卡死
import time; time.sleep(1)

# 对:await asyncio.sleep(1) 只是当前协程挂起
await asyncio.sleep(1)

这是协程最经典的坑:协程里一旦调了同步阻塞函数(重计算、同步 SQL、requests.get、time.sleep、大锁、pandas 大数据处理),整个单线程 loop 都被占住,别人谁也跑不了。

6. 超时:asyncio.wait_for

python 复制代码
try:
    r = await asyncio.wait_for(call_api(), timeout=3.0)
except asyncio.TimeoutError:
    ...

对外调 API、远程 DB、RPC 一定加超时,否则对方卡住,协程和连接一直挂着。

7. 限流:别无脑 gather(100000) ------ 用 Semaphore

python 复制代码
sem = asyncio.Semaphore(20)

async def fetch(u):
    async with sem:
        return await real_fetch(u)

爬虫/批量调第三方 API 时,10000 个任务一起 gather 会把对方打挂、自己也 FD/内存炸;信号量控制"同时只有 20 个飞行中请求"很常见。

8. 生产者消费者:asyncio.Queue

实时数据、爬虫待抓队列、消息消费、日志管道:

python 复制代码
queue = asyncio.Queue()

async def producer():
    for i in range(10):
        await queue.put(i)
        await asyncio.sleep(0.1)

async def consumer(name):
    while True:
        item = await queue.get()
        print(name, item)
        queue.task_done()

asyncio 官方也提供 Queue 来做任务分发/同步并发代码。

9. 协程里混阻塞 IO:run_in_executor

真要读大文件、调 C 库、跑 sklearn/pandas/numpy 重计算、requests.get?别裸跑:

python 复制代码
def sync_work():
    time.sleep(2)
    return 1

r = await asyncio.to_thread(sync_work)          # Python 3.9+
# 老版:loop.run_in_executor(None, sync_work)

这会丢到线程池跑,不让主 loop 被堵。但注意:这是"借线程池救火",不是协程变快;CPU 密集还得多进程

10. 后台任务 / 心跳 / 消费循环

Web 服务里后台刷缓存、消费 Redis stream、websocket ping、消费 kafka:

python 复制代码
async def worker(q):
    while True:
        item = await q.get()
        ...

tg = asyncio.TaskGroup()
tg.create_task(worker(q))
tg.create_task(web_server())

但要管好 cancel/shutdown:服务退出时给 stop flag / asyncio.Event(),别留孤儿任务。


三、常见"协程库"到底指哪几类

1. 标准库 asyncio ------ 地基

asyncio 不是某个业务库,它是地基:event loop、Task、Future、Queue、Lock/Semaphore/Event、subprocess、TCP/UDP/pipe、超时取消、线程桥接......都它给。官方说它是很多异步 Web 框架、DB 库、分布式队列的底层基础。

2. 异步驱动:aiohttp / httpx / asyncpg / aiomysql / aioredis / Motor / aiofiles

这才是协程真正爽的地方 :要"等 HTTP / 等 DB / 等 Redis / 等磁盘",就必须用原生支持 asyncio 的 driver/lib ,不能只在函数头写 async def 然后里面 requests.get()------那个 requests.get() 是同步阻塞,照样堵死 loop。

  • aiohttp :异步 HTTP 客户端 + 服务端,官方示例就 async with aiohttp.ClientSession() + session.get();也能写 Web Server / WebSocket,适合爬虫、API 聚合、微服务、实时推送
  • httpx:现代 HTTP client,同步/异步都支持,调用多第三方 API 时代码干净
  • asyncpg:PostgreSQL 异步驱动,async/await 查 PG,API/DB 里"等 SQL"不堵 loop
  • aiomysql / aioredis / motor / aiofiles:MySQL 异步、Redis 异步、MongoDB Motor、异步文件

生态大致是:HTTP aiohttp/httpx、PG asyncpg、MySQL aiomysql、Mongo Motor、Redis aioredis、FS aiofiles、Web FastAPI/Sanic/aiohttp/Tornado、测试 pytest-asyncio

3. 异步 Web 框架:FastAPI / Sanic / aiohttp / Tornado / Quart

FastAPI 是现代 Python API 服务主流:写 async def endpoint(),里面 await DB/HTTP/Redis,一个 worker 能扛很多"等 IO"的连接。官方/生态材料里它 REST API、微服务、中台、BFF、高并发后端都很合适;Tornado/aiohttp 在 WebSocket/长连接/老实时系统也有位置。

python 复制代码
from fastapi import FastAPI
import httpx, asyncio

app = FastAPI()

async def get_prices():
    async with httpx.AsyncClient() as c:
        r = await c.get("https://api.xxx/prices")
        return r.json()

@app.get("/v1/prices-with-cache")
async def api():
    prices = await get_prices()   # 等外部 API 时 loop 能接别的请求
    return {"prices": prices}

4. 老派"猴子补丁"协程:gevent / eventlet

它们不走 async/await,而是把 socket/time 等底层替换成绿色线程/greenlet,让同步代码"看起来同步、实际 IO 会让出"。好处是老代码/requests/老 MySQL driver 有时不重写就"协程化";坏处是 monkey patch 黑魔法、调试难、生态新代码越来越 async native新项目我建议 asyncio/aio 原生;老巨石项目、历史爬虫/服务、团队不懂 async 又想 IO 并发,才可能选 gevent

5. Trio / anyio ------ 更安全的结构化并发思路

asyncio 是官方/生态之王;Trio 强调"更简单安全的取消/ nursery/结构化并发",AnyIO 想给它和 asyncio 搭桥。写业务 API 还是 asyncio/FastAPI 为主;写底层高可靠并发库、网络协议库、想避开 asyncio 某些 callback/future 复杂度时可研究 Trio/AnyIO。


四、协程最适合的 8 个场景

场景 1:批量 HTTP / 爬虫 / API 聚合

顺次调 100 个 URL,每个 200ms,同步要 20s;协程里 100 个请求一起飞,理想接近 200ms~几秒(受限于对方 RT/带宽/DNS/连接池)。RealPython/教程也说 10 个 1s delay 串行 ~10s,asyncio gather 并发 ~1s------差别不是 CPU 快,是"等网络时别人继续发"。

python 复制代码
async def fetch_all(urls):
    async with aiohttp.ClientSession() as s:
        tasks = [s.get(u) for u in urls]
        resps = await asyncio.gather(*tasks)
        return [await r.text() for r in resps]

适合:爬虫、价格监控、批量 webhook、OpenAPI embedding/RAG 调 LLM API、中台 BFF 聚合用户/订单/支付/推荐接口。

场景 2:高并发 Web API / FastAPI 后端

FastAPI 里 endpoint 是 async def,里面 await DB/Redis/HTTP------一个 uvicorn worker 在等 DB 时还能接别的 HTTP 请求,吞吐远高于"一个请求一个线程傻等 DB"。但记住:如果 endpoint 里 time.sleep(5)requests.get()pandas.read_csv(10G),async 就废了,因为 loop 被堵死。

正确姿势:

python 复制代码
@app.get("/user/{uid}")
async def get(uid: int):
    user = await db.fetchrow("select * from users where id=$1", uid)
    profile = await httpx.AsyncClient().get(profile_svc)
    return {...}

场景 3:异步数据库 / Redis / MQ / Mongo

Web 服务最大瓶颈往往不是 Python 算,而是"等 PG/MySQL/Redis/Kafka/Mongo"。用 asyncpg/aioredis/Motor/aiokafka,一个请求在 await conn.fetch() 时,loop 还能接别的请求;用同步 driver 也行,但最好配 run_in_executor 或连接池线程池,否则 sync driver 会堵 loop。

场景 4:WebSocket / 长连接 / 实时推送

聊天室、行情推送、日志 tail、终端 Web Terminal、IM、游戏小包长连、AI streaming SSE/WS------一个服务可能扛几万长连接,它们 99% 时间在"等下一条消息",不是算;协程/事件循环比一个连接一个线程便宜太多。aiohttp/websockets/FastAPI WS/Tornado 都吃这场景。

场景 5:消息消费 / 任务流水线

consumer 从 Redis Stream / Kafka / RabbitMQ 拿消息 → enrich → 调外部 API → 写 DB → ack,中间一堆 IO,asyncio Queue/Semaphore/TaskGroup 搭生产者消费者很自然:

python 复制代码
# 伪代码
queue = asyncio.Queue(maxsize=100)

async def producer():
    while True:
        msg = await redis.xreadgroup(...)
        await queue.put(msg)

async def worker(i):
    while True:
        msg = await queue.get()
        await call_api(msg)
        await db.execute(...)

场景 6:高并发微服务网关 / BFF

BFF 常干"前端一个页面要用户/商品/推荐/营销/权限/库存",同步串起来 5×100ms=500ms;async 并发 await asyncio.gather(get_user(), get_goods(), get_rec()) 理论 ~100ms。这就是 API 网关、中台聚合、BFF 爱 async 的原因:不是 CPU 快,是外部 IO 并发等。

场景 7:轻量实时系统 / IoT / 日志采集 agent

一个 agent 管 5000 设备长连、收心跳、写 Kafka、调告警、批刷本地 buffer------协程比 5000 线程轻;但如果 agent 还要本地做实况音视频编码/大模型推理/zip 10GB,那 CPU 部分丢进程池

场景 8:测试 / mock / 高并发压测工具

自己写轻量压测、批量测 1000 个 API token、批量 webhook replay、mock 供应商回调风暴------协程版 client 一台机器轻松发很多飞行中 HTTP,比多线程脚本轻。


五、协程不适用的场景 / 大坑

坑 1:CPU 密集 ≠ 协程

图片 resize、PDF 生成、大模型 forward、pandas 10G agg、AES 重加密、hashcash------这些都是真吃 CPU;协程仍是单线程,一个协程占 CPU 算 factorial(big),别人全等它算完。官方 asyncio 适合 IO-bound/网络;CPU-bound 看 ProcessPoolExecutor/多进程。

python 复制代码
# 错:CPU 炸弹
async def bomb():
    while True:
        x = x * x  # loop 死了

# 对:丢进程池
loop = asyncio.get_running_loop()
r = await loop.run_in_executor(process_pool, cpu_work)

坑 2:async def ≠ 真的异步

python 复制代码
async def fake_async():
    requests.get("https://example.com")   # 还是同步阻塞!
    time.sleep(1)                         # 还是堵 loop!

名字叫 async,身体很同步。真异步必须一路都是 await 非阻塞/async driver:aiohttp/httpx.AsyncClient/asyncpg/aioredis/Motor 才是真 async;requests/time.sleep/pymysql.sync/pandas 全是 loop 杀手

坑 3:async All the Things 会传染

一旦顶层 async,里面调用链都想 async;ORM 是同步 SQLAlchemy/Django ORM old style、SDK 是 requests、PDF 是 sync、Excel 是 pandas,那 async 边界就很痛。现实选型:新 FastAPI + asyncpg/httpx/Motor 很香;老 Django/Flask/数据分析脚本别硬改,先线程池/进程池

坑 4:无脑 gather 一万任务

python 复制代码
# 危险
await asyncio.gather(*[fetch(u) for u in 100000_urls])

正确:信号量 / 分批 / aiohttp TCPConnector limit / 队列 worker pool:

python 复制代码
sem = asyncio.Semaphore(50)
async def safe_fetch(u):
    async with sem:
        return await real_fetch(u)

坑 5:忘了超时 / 忘了 close

HTTP ClientSession / Redis / DB conn / WS 都要关;外部 API 要 timeout;任务要能 cancel。aiohttp 官方示例都用 async with ClientSession() / async with session.get() 保证关响应/关 session。


六、工程选型:线程 / 多进程 / 协程怎么选

场景 推荐
简单脚本、同事不懂 async、主要 CPU 同步 / 多进程
少量 IO、想简单、老代码重 线程池 ThreadPoolExecutor
高并发 HTTP/DB/Redis/长连接/网关 协程 asyncio + aio 驱动
CPU 重(cv/ml/压缩/加密/agg) 协程只做 IO,CPU 丢 ProcessPool
老 monolith + requests + mysqlclient 想不改代码 gevent(黑魔法)
新 API / RAG 调 LLM / 实时 WS FastAPI + httpx/asyncpg/aioredis

一句话:等待多(网络/DB/Redis/RPC/WS)→ 协程;计算多(numpy/pandas/ml/压缩)→ 多进程;旧同步 SDK 很多又不想全改 → 线程池/gevent 过渡


七、一个"生产感"模板:带限流/超时/结果的批量爬虫

python 复制代码
import asyncio, aiohttp

sem = asyncio.Semaphore(20)

async def fetch(session, url, timeout=5):
    async with sem:
        try:
            async with asyncio.timeout(timeout):
                async with session.get(url) as resp:
                    resp.raise_for_status()
                    text = await resp.text()
                    return {"url": url, "ok": True, "text": text[:100]}
        except Exception as e:
            return {"url": url, "ok": False, "error": str(e)}

async def batch(urls):
    async with aiohttp.ClientSession() as s:
        tasks = [fetch(s, u) for u in urls]
        return await asyncio.gather(*tasks)

if __name__ == "__main__":
    urls = ["https://example.com"] * 100
    r = asyncio.run(batch(urls))
    print(sum(x["ok"] for x in r))

要点:ClientSession 复用、Connector 控连接数、Semaphore 限并发、timeout 防挂死、gather 拿全部结果、async with 关资源------这就是"能上线"的协程代码,不是 demo。


八、项目怎么落地

新项目、高并发 IO、微服务 API、爬虫、WebSocket、网关 → 上 asyncio 生态 :FastAPI + httpx/asyncpg/aioredis/Motor + asyncio.gather/TaskGroup/Semaphore/timeout

但别迷信协程:endpoint 里有 requests.get 就换 httpx/aiohttp;有 time.sleepasyncio.sleep;有 pandas 大表/模型推理/zip 加密就 asyncio.to_thread/run_in_executor/ProcessPool;CPU 重就多进程;老小脚本别为了炫技 async。

最精炼的一句话:协程是用 async/await 写的"合作式 IO 并发"------等别人响应时让出 CPU,所以网络/DB/Redis/WS/爬虫/网关/长连接场景极香;但只要里面是纯算/同步阻塞,它就只是披着 async 皮的同步阻塞。

相关推荐
aqi001 小时前
15天学会AI应用开发(十七)使用LangGraph实现会话记忆功能
人工智能·python·大模型·ai编程·ai应用
第一程序员2 小时前
Rust Agent 子进程执行:Command 之前,先定义输入和超时
python·rust·github
skywalk81632 小时前
设计并实现段言的 C FFI 绑定机制 @Trae
c语言·开发语言·python·编程
weixin_BYSJ19872 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php
EQUINOX14 小时前
【论文阅读】| MoCo精读
论文阅读·人工智能·python·深度学习·机器学习
用户8356290780514 小时前
使用 Python 自动化 Excel 公式和函数:完整指南
后端·python
敲代码的嘎仔5 小时前
实习日志day6--实习日志day6--title命名规范化&businessType纠正&补充缺失的@Log注解&报警与通信模块补充&产出阶段总结文档
java·开发语言·人工智能·git·python·实习·大二
江华森5 小时前
Python 实现高德地图找房(一):环境搭建与数据爬虫
开发语言·爬虫·python
VIP_CQCRE6 小时前
用 Ace Data Cloud 快速接入 OpenAI Chat Completions:对话、流式、多轮和多模态一篇打通
python·ai·openai·api·教程