Python异步库实测:asyncio、uvloop、trio等10个库选型指南
2026/9/8 2:46:46 网站建设 项目流程

如果你已经写过async defawait,大概率也经历过这种困惑:函数明明写了 await,一运行却提示coroutine was never awaited;换用aiohttp后并发确实上去了,服务端却开始疯狂断开连接;再用trio重写,又发现生态里的某些库根本不兼容。我这次把市面上常见的 10 个 Python 异步库全部拉出来跑了一遍,包括标准库asyncio、加速事件循环的uvloop、结构化并发的trio、兼容抽象的anyio、原生化实验库curio、HTTP 领域的aiohttphttpx,以及数据库方向的asyncpgaiosqliteaiomysql。测完以后,我算是真正理解了 async/await 为什么这么火,也把每条坑都踩了一遍。

这篇文章不是给你一个“谁最快”的排行榜,而是告诉你每个库到底解决什么问题、适合什么场景、用起来有哪些隐藏前提。不管你是刚开始学异步,还是已经在项目里踩坑,看完都能直接照着做。

1. 测试背景:10个库是怎么选出来的

1.1 我的实际业务场景

我这次测试的起因是一个数据采集项目:本地需要从内网 API 拉取大约 5000 条业务数据,每条的响应时间在 20ms 到 200ms 之间,绝大多数时间都花在网络等待上。最初版本用requests写同步循环,跑完一轮需要十几分钟,后来改成多线程,虽然快了一些,但线程一多 CPU 占用和内存都不太好看,而且一旦某个接口超时,线程容易堆积。

于是我把重心转移到了 Python 异步生态上。项目是典型的 IO 密集型场景,非常适合async/await。我没有直接选框架,而是想先把底层几个核心库的真实表现摸清楚,所以一次挑了 10 个有代表性的异步库来做横向测试。

1.2 10个库的分组逻辑

10 个库看起来多,但本质上可以分成四组:

  • 事件循环与底层控制asynciouvloopcurio
  • 结构化并发与兼容层trioanyio
  • HTTP 客户端aiohttphttpx
  • 数据库驱动asyncpgaiosqliteaiomysql

我刻意没有把FastAPIQuart这类 Web 框架算进去,因为框架本身不是异步库,而是异步库的上层封装。测试“库”而不是“框架”,才能看清楚底层机制之间的差异。

1.3 环境准备:Python 安装与虚拟环境

测试环境是 Python 3.11.7,操作系统是 macOS,CPU 是 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 python@3.11

Linux 可以用系统包管理器,也可以源码编译。无论哪种方式,我都建议在项目目录里先建一个虚拟环境:

python3.11 -m venv .venv source .venv/bin/activate pip install --upgrade pip

这一步别看简单,能避免很多第三方异步库因为系统 Python 环境混乱而出现的版本冲突。接下来安装本次测试所需的核心库:

pip install aiohttp httpx uvloop trio anyio curio asyncpg aiosqlite aiomysql

2. 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又想做一个统一抽象层,让你同一套代码在asynciotrio后端之间随意切换。所以,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、anyio

trio最大的卖点是 “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可以理解成“异步后端适配器”。它把asynciotrio都封装成了统一接口,业务代码只依赖anyioTaskGrouprunto_thread等 API,运行时再切后端。我用 anyio 写了一段和trio几乎一样的业务代码,后端切换非常顺滑。FastAPI 底层其实也大量使用了 anyio,所以不少 FastAPI 用户会意外发现自己的异步代码已经跑在 trio 上了。

3.3 HTTP 客户端:aiohttp、httpx

HTTP 是 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:pass@localhost/mydb", min_size=2, max_size=10, )

连接池可以显著降低频繁建连的开销。我在本地压测中,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 秒串行,最直观的 baseline
asyncio + 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, timeout=10) 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_redirects=True, limits=httpx.Limits(max_connections=CONCURRENCY) ) as client: tasks = [fetch_one(client, sem, f"https://example.com/api/{i}") for i in range(200)] pages = await asyncio.gather(*tasks) asyncio.run(main())

这段代码里有两个容易被忽略的细节:第一,SemaphoreAsyncClient的连接数要配合,不能并发限制 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会被抛出,之前的请求也会被正确取消。在trioanyio里,也有对应的move_on_afterfail_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 对象引用,或者没有等待所有任务完成就退出了。解决办法是统一收集任务并await

tasks = [asyncio.create_task(fetch_one(i)) for i in range(10)] await asyncio.gather(*tasks, return_exceptions=True)

trio里这个坑会自动规避,因为 Nursery 语法强制要求所有任务在退出作用域前结束,等不到就取消任务并抛错。

5.3 并发一上去就报 Connection reset

这种问题一般不是异步库本身的问题,而是对端服务或网络中间件做了连接数限制。排查思路是先逐步降低并发数,比如从 200 降到 50,再看连接是否稳定。如果并发降到 20 还是报错,可以检查本地端口是否耗尽,或者服务端是否有防火墙限制。注意httpxmax_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
  • 如果对任务取消、异常传播要求严格,推荐trioanyio,结构化并发能挡掉不少隐蔽 bug。
  • 数据库优先asyncpg,MySQL 用aiomysql,小工具本地文件存储用aiosqlite

最后再分享一点我个人的体会:async/await 并不是银弹。对于计算密集型任务,该用多进程还是用多进程;对于 IO 密集型任务,也不是无脑上异步就一定更快,还要考虑对端服务的并发承受能力。测试完这 10 个库以后,我最大的收获是学会了“先想清楚瓶颈在哪,再决定用哪个库”,而不是上来先选一个看起来很潮的技术栈。技术栈永远是工具,业务目标才是主线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询