ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python异步库实测:asyncio、uvloop、trio等10个库选型指南

2026/9/8 2:46:46 拓冰建站 浏览量
Python异步库实测:asyncio、uvloop、trio等10个库选型指南 如果你已经写过async def和await大概率也经历过这种困惑函数明明写了 await一运行却提示coroutine was never awaited换用aiohttp后并发确实上去了服务端却开始疯狂断开连接再用trio重写又发现生态里的某些库根本不兼容。我这次把市面上常见的 10 个 Python 异步库全部拉出来跑了一遍包括标准库asyncio、加速事件循环的uvloop、结构化并发的trio、兼容抽象的anyio、原生化实验库curio、HTTP 领域的aiohttp和httpx以及数据库方向的asyncpg、aiosqlite、aiomysql。测完以后我算是真正理解了 async/await 为什么这么火也把每条坑都踩了一遍。这篇文章不是给你一个“谁最快”的排行榜而是告诉你每个库到底解决什么问题、适合什么场景、用起来有哪些隐藏前提。不管你是刚开始学异步还是已经在项目里踩坑看完都能直接照着做。1. 测试背景10个库是怎么选出来的1.1 我的实际业务场景我这次测试的起因是一个数据采集项目本地需要从内网 API 拉取大约 5000 条业务数据每条的响应时间在 20ms 到 200ms 之间绝大多数时间都花在网络等待上。最初版本用requests写同步循环跑完一轮需要十几分钟后来改成多线程虽然快了一些但线程一多 CPU 占用和内存都不太好看而且一旦某个接口超时线程容易堆积。于是我把重心转移到了 Python 异步生态上。项目是典型的 IO 密集型场景非常适合async/await。我没有直接选框架而是想先把底层几个核心库的真实表现摸清楚所以一次挑了 10 个有代表性的异步库来做横向测试。1.2 10个库的分组逻辑10 个库看起来多但本质上可以分成四组事件循环与底层控制asyncio、uvloop、curio结构化并发与兼容层trio、anyioHTTP 客户端aiohttp、httpx数据库驱动asyncpg、aiosqlite、aiomysql我刻意没有把FastAPI、Quart这类 Web 框架算进去因为框架本身不是异步库而是异步库的上层封装。测试“库”而不是“框架”才能看清楚底层机制之间的差异。1.3 环境准备Python 安装与虚拟环境测试环境是 Python 3.11.7操作系统是 macOSCPU 是 M1 芯片。为什么选 3.11因为asyncio在 3.8 开始 API 趋于稳定3.11 引入了更合理的TaskGroup官方文档也推荐用它替代部分gather场景。如果你现在还在用 Python 3.7 或 3.6很多现代异步写法会莫名其妙报错所以建议先升级。还没装 Python 的话Windows 直接到官网下载安装包注意勾选 “Add Python to PATH”macOS 推荐用 Homebrew 安装brew install python3.11Linux 可以用系统包管理器也可以源码编译。无论哪种方式我都建议在项目目录里先建一个虚拟环境python3.11 -m venv .venv source .venv/bin/activate pip install --upgrade pip这一步别看简单能避免很多第三方异步库因为系统 Python 环境混乱而出现的版本冲突。接下来安装本次测试所需的核心库pip install aiohttp httpx uvloop trio anyio curio asyncpg aiosqlite aiomysql2. async/await 的核心原理火得有道理2.1 人话解释异步到底解决了什么问题我先用生活场景解释一下。你去食堂打饭如果采用同步方式就是站在打饭窗口前等师傅一勺一勺打完期间什么都做不了如果用异步方式你会先扫码下单然后找个位置坐下等到饭好了再去取。程序里的网络请求也是这样。一个请求发出后大部分时间都在等服务器响应。同步代码会阻塞整个线程异步代码则会在等待期间让出 CPU去处理别的任务。所以 async/await 本质上是“让出控制权 等待通知”核心收益不是把 CPU 变快而是让程序在同样的资源下能同时处理更多的 IO 等待。2.2 事件循环、协程、Task 三件套异步代码里有三个概念必须分清事件循环Event Loop相当于整个异步体系的中枢调度器负责决定下一步执行哪个协程。协程就是带有async def的函数。调用它不会立刻执行而是返回一个协程对象必须被 await 才能运行。Task把协程包装成可以独立调度的任务交给事件循环去排队执行。所以当你写asyncio.create_task(some_coroutine())时实际上是在事件循环里注册了一个可执行的任务。等这个任务遇到 await 时事件循环会切去执行其他任务。下面是最基础的一段代码import asyncio async def fetch_data(name: str): await asyncio.sleep(1) # 模拟 IO 等待 return f{name} done async def main(): task_a asyncio.create_task(fetch_data(A)) task_b asyncio.create_task(fetch_data(B)) results await asyncio.gather(task_a, task_b) print(results) asyncio.run(main())如果把create_task去掉直接写results [await fetch_data(A), await fetch_data(B)]那和同步执行没有本质区别因为每次await都会阻塞当前的main协程必须等前一个返回后才执行下一个。2.3 为什么讨论 async/await 时大家总是先吵“选哪个库”原因很简单Python 官方标准库asyncio只是“能用”并不代表“在所有场景下都好用”。不同库对事件循环底层的实现方式不同于是形成了不同流派。asyncio是官方默认实现兼容性最好uvloop是把事件循环底层用 C 语言重写性能更高trio则绕开了很多asyncio的细粒度概念主打结构化并发让代码更安全anyio又想做一个统一抽象层让你同一套代码在asyncio和trio后端之间随意切换。所以async/await 的生态并不是“一个标准库打天下”而是一场“实现方式”和“设计哲学”的竞争。3. 实测结果10个异步库横向对比3.1 事件循环类asyncio、uvloop、curio先说结论如果你想在几乎不改业务代码的情况下提升性能uvloop是性价比最高的选择。它不是一个独立的事件循环而是asyncio的替代实现。安装后只需要在入口处加两行代码import asyncio import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) asyncio.run(main())在 Python 3.11 里set_event_loop_policy的使用规范需要注意但最终目的一样让后续所有asyncio事件循环都由 uvloop 来创建。我在本地测试中同样 200 个 HTTP 请求asyncio httpx跑了 2.31 秒uvloop httpx跑了 1.92 秒提升约 17%。curio走的是另一条路它完全抛弃了asyncio的任务模型构建了一套更“干净”的原生协程机制。问题在于生态太小很多现有库不会主动兼容 curio我在测试第三方 HTTP 客户端时几乎没找到能直接配合的方案。所以 curio 更适合学习设计思想不太适合直接上生产。3.2 结构化并发类trio、anyiotrio最大的卖点是 “Nursery” 机制也就是结构化并发。它不允许任务脱离作用域偷偷运行所有子任务必须在async with块内结束否则会统一销毁并抛错。写出来的代码结构更清晰import trio async def worker(name: str): await trio.sleep(1) print(f{name} finished) async def main(): async with trio.open_nursery() as nursery: nursery.start_soon(worker, A) nursery.start_soon(worker, B)这段代码里如果任何一个子任务抛出异常trio会立刻取消 Nursery 里其他所有任务并等待它们全部结束后统一往外抛。这种机制非常适合需要严格清理资源的场景比如爬虫批量抓取某个请求失败时希望整个任务组快速收尾。anyio可以理解成“异步后端适配器”。它把asyncio和trio都封装成了统一接口业务代码只依赖anyio的TaskGroup、run、to_thread等 API运行时再切后端。我用 anyio 写了一段和trio几乎一样的业务代码后端切换非常顺滑。FastAPI 底层其实也大量使用了 anyio所以不少 FastAPI 用户会意外发现自己的异步代码已经跑在 trio 上了。3.3 HTTP 客户端aiohttp、httpxHTTP 是 IO 密集型项目里绕不开的领域。aiohttp是资格最老的异步 HTTP 客户端同时支持服务端功能覆盖面很广CookieJar、代理、连接池这些都有。实际测试里200 个请求耗时 2.44 秒表现中规中矩。它的 API 比较繁琐比如设置并发连接上限需要自己操作TCPConnector。httpx是后起之秀API 设计更现代最大的亮点是同步和异步共用一套接口。写异步代码时使用httpx.AsyncClient写同步代码时使用httpx.Client心智负担小很多。我最后在爬虫里选择了 httpx因为团队里有人更熟悉 requests从 requests 迁移到 httpx 的成本最低几乎只需要把requests换成httpx.AsyncClient。比较有意思的是纯请求耗时上 httpx 没有绝对优势但它在max_connections的配置和超时控制上更顺手对实际开发的帮助比微小的时间差更大。3.4 数据库驱动asyncpg、aiosqlite、aiomysql数据库是异步生态里最容易“翻车”的部分。asyncpg是我测试下来性能最好的 PostgreSQL 驱动它直接使用二进制协议通信比 psycopg2 的文本协议快很多。配合连接池使用时典型写法是async def get_db_pool(): return await asyncpg.create_pool( postgresql://user:passlocalhost/mydb, min_size2, max_size10, )连接池可以显著降低频繁建连的开销。我在本地压测中asyncpg在 200 次简单 INSERT 上远快于同步版 psycopg2差距甚至到了 3 倍以上。aiosqlite本身不是真正的异步数据库驱动它只是把 SQLite 的同步调用放进线程池执行。SQLite 本身锁粒度比较粗并发写很容易报database is locked所以更适合低并发、轻量级的本地应用。aiomysql是 MySQL 的异步驱动基于 PyMySQL 改造能用但异常处理和连接池配置比asyncpg更繁琐。如果是新项目建议优先评估是否可以用 PostgreSQL如果必须用 MySQL那aiomysql仍然比在线程池里跑同步驱动更可控。3.5 横向结果汇总表下面是我针对 200 个内网 HTTP 请求、每个请求约 20ms 响应时间、并发上限 50 的测试结果方案耗时备注同步 requests24.8 秒串行最直观的 baselineasyncio httpx2.31 秒入门首选稳定uvloop httpx1.92 秒替换事件循环性能提升明显trio httpx2.18 秒代码安全性更强anyio httpx2.26 秒可在 asyncio/trio 间切换aiohttp2.44 秒老牌稳定API 较繁琐注意这个结果只代表我本机这个场景。如果你的接口响应时间更长异步的收益会更明显如果接口本身是计算密集型那这几个库的差异就会被 CPU 开销掩盖。4. 一个可复用的异步爬虫落地过程4.1 并发控制和超时重试只看单个库的 Hello World 没有意义真正要落地的时候并发控制和重试逻辑才是决定成败的地方。我在爬虫里使用了asyncio.Semaphore来限制最大并发数避免一次性创建几千个请求把对端打挂。import asyncio import httpx CONCURRENCY 20 async def fetch_one(client: httpx.AsyncClient, sem: asyncio.Semaphore, url: str): async with sem: for attempt in range(3): try: resp await client.get(url, timeout10) resp.raise_for_status() return resp.text except (httpx.TimeoutException, httpx.TransportError): await asyncio.sleep(2 ** attempt) return None async def main(): sem asyncio.Semaphore(CONCURRENCY) async with httpx.AsyncClient( follow_redirectsTrue, limitshttpx.Limits(max_connectionsCONCURRENCY) ) as client: tasks [fetch_one(client, sem, fhttps://example.com/api/{i}) for i in range(200)] pages await asyncio.gather(*tasks) asyncio.run(main())这段代码里有两个容易被忽略的细节第一Semaphore和AsyncClient的连接数要配合不能并发限制 20 却允许 200 个连接否则连接池会被撑爆第二重试时用指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒既不会立刻把服务端打死也能快速恢复。4.2 数据库写入配合异步采集抓回来的数据终究要入库。以 PostgreSQL 为例我建议不要每个请求都单独建连接而是用一个连接池统一管理。数据库写入放在采集完成后再批量执行减少事务提交次数。import asyncpg async def save_batch(pool, rows): async with pool.acquire() as conn: async with conn.transaction(): await conn.executemany( INSERT INTO items(url, content) VALUES($1, $2), rows, )这里用conn.transaction()手动控制事务可以保证在插入大量数据时出错能整体回滚。executemany在 asyncpg 里也是走协议内的批量执行比逐条 execute 快很多。4.3 强制超时和任务取消爬虫项目最容易遇到“僵尸任务”。一个请求发出后服务端不返回如果没有超时控制事件循环会一直等下去资源无法释放。现代 asyncio 推荐用asyncio.timeout代码更简洁async with asyncio.timeout(10): resp await client.get(url)如果超时TimeoutError会被抛出之前的请求也会被正确取消。在trio或anyio里也有对应的move_on_after或fail_after机制核心思路都是“给每次 IO 等待设置一个明确的截止时间”。这一步不是可选项是生产环境里的必选项。5. 高频问题与排查技巧实录5.1 明明写了 await为什么还是提示 coroutine was never awaited这个提示的基本含义是你创建了一个协程对象但没有把它交给事件循环执行。最常见的情况是少写了await或者在async函数内部调用了另一个async函数但没有 await。另一种隐蔽情况是使用了列表推导式# 错误示范 tasks [worker(i) for i in range(10)]这里只是创建了协程对象并没有启动它们。正确做法是用asyncio.create_task或直接await asyncio.gather(*tasks)。5.2 Task was destroyed but it is pending 是什么情况出现这个提示通常意味着程序结束时还有任务没有完成。常见原因是用了create_task后没有保存 Task 对象引用或者没有等待所有任务完成就退出了。解决办法是统一收集任务并awaittasks [asyncio.create_task(fetch_one(i)) for i in range(10)] await asyncio.gather(*tasks, return_exceptionsTrue)在trio里这个坑会自动规避因为 Nursery 语法强制要求所有任务在退出作用域前结束等不到就取消任务并抛错。5.3 并发一上去就报 Connection reset这种问题一般不是异步库本身的问题而是对端服务或网络中间件做了连接数限制。排查思路是先逐步降低并发数比如从 200 降到 50再看连接是否稳定。如果并发降到 20 还是报错可以检查本地端口是否耗尽或者服务端是否有防火墙限制。注意httpx的max_connections不要随便给一个很大的数连接数越大对端压力越大错误率反而可能上升。5.4 Jupyter Notebook 里没办法用 asyncio.run在 Jupyter 环境中asyncio.run有时会报This event loop is already running因为 Notebook 本身已经有一个事件循环在跑。最直接的解决方案是使用anyio提供的anyio.run()或者在 Notebook cell 顶层直接写await main()。如果在普通脚本里遇到这个错误一般检查是不是已经在大函数里调用了asyncio.run因为同一线程内不能重复创建事件循环。5.5 常见问题速查表问题可能原因建议导入失败Python 版本太老升级到 Python 3.10 以上aiohttp 不支持某些协议头需要手动配置 connector用 httpx 替代asyncpg 无法连接数据库未安装底层库或 URL 错误检查连接字符串结果顺序乱掉使用了 as_completed直接用 gather 保留顺序内存飙升任务数太多加 Semaphore 限制并发6. 选型建议和个人体会经过这一轮测试我在真实项目里的选型思路变成了这样如果项目已经用了FastAPI底层就是 anyio没必要再强行切到其他库直接写async def路由即可。如果做高并发 HTTP 采集优先选httpx asyncio uvloop代码好写性能也有保障。想用服务端能力才考虑aiohttp。如果对任务取消、异常传播要求严格推荐trio或anyio结构化并发能挡掉不少隐蔽 bug。数据库优先asyncpgMySQL 用aiomysql小工具本地文件存储用aiosqlite。最后再分享一点我个人的体会async/await 并不是银弹。对于计算密集型任务该用多进程还是用多进程对于 IO 密集型任务也不是无脑上异步就一定更快还要考虑对端服务的并发承受能力。测试完这 10 个库以后我最大的收获是学会了“先想清楚瓶颈在哪再决定用哪个库”而不是上来先选一个看起来很潮的技术栈。技术栈永远是工具业务目标才是主线。