1. asyncio 的局限与痛点
asyncio 是 Python 标准库中的异步编程框架,自 Python 3.4 引入以来已经发展成熟。然而,在生产环境和高并发场景下,asyncio 暴露出一系列设计上的缺陷:
1.1 缺乏结构化并发
asyncio 的任务创建是「发射后不管」的模式:
import asyncio
async def main():
task = asyncio.create_task(some_background_work())
# task 在后台运行,主函数不会等待它完成
# 如果主函数提前结束,task 可能还在运行,造成资源泄漏
这导致任务生命周期管理困难——后台任务可能悄悄失败、资源无法正确回收、异常被吞没。
1.2 取消机制脆弱
asyncio 的任务取消依赖 asyncio.CancelledError,但:
- 取消不能跨越
asyncio.shield()与asyncio.wait_for()的边界保证强一致性 - 取消可能在任意
await点抛出异常,代码必须处处处理 TaskGroup(Python 3.11+)虽然有所改善,但仍是后期补丁
1.3 超时处理繁琐
# asyncio 的超时写法显得冗长且容易出错
try:
await asyncio.wait_for(fetch_data(), timeout=5.0)
except asyncio.TimeoutError:
# 超时后 fetch_data 可能仍在后台运行,无法真正中断
pass
wait_for() 超时后,被包裹的协程只是被丢弃,而非真正取消——这会在 I/O 操作涉及资源锁定时造成死锁。
1.4 回调地狱隐患
asyncio 的历史包袱包含基于回调的旧 API(loop.call_soon、add_done_callback),与现代 async/await 风格并存,增加了心智负担。
2. Trio 的设计哲学
Trio 是一个从零设计的第三方异步库,它的核心思想可以概括为一句话:
结构化并发(Structured Concurrency) —— 并发代码的控制流结构应当与同步代码一样清晰。
Trio 的设计深受论文 “Structured Concurrency” 影响,主张用嵌套作用域管理并发任务,而非平坦的任务列表。
2.1 Nursery:任务生命周期管理
Trio 用 nursery(托儿所)替代了 asyncio 的 create_task:
import trio
async def worker(name: str, sleep_time: float):
await trio.sleep(sleep_time)
print(f"{name} 完成")
async def main():
async with trio.open_nursery() as nursery:
nursery.start_soon(worker, "A", 1.0)
nursery.start_soon(worker, "B", 0.5)
nursery.start_soon(worker, "C", 2.0)
# nursery 代码块结束时,所有子任务必须已经完成
print("所有任务完成")
trio.run(main)
关键区别:
async with退出前,所有通过nursery.start_soon()启动的任务都必须自然结束或被取消- 这意味着任务树形成严格的嵌套层次——父任务不会比子任务先结束
- 异常会从子任务自动冒泡到父任务
2.2 禁止「孤儿任务」
Trio 完全禁止了无管理的后台任务。任何并发都必须显式挂载在某个 nursery 下。这看似限制,实则是防止资源泄漏的强约束。
3. AnyIO:Trio 与 asyncio 的兼容桥梁
Trio 的 API 设计优雅,但 Python 生态大部分异步库(如 aiohttp、asyncpg)只支持 asyncio。AnyIO 应运而生——它是一个抽象层,让你的代码可以同时运行在 Trio 和 asyncio 两个后端之上。
pip install anyio[trio] httpx
import anyio
async def task(name: str):
await anyio.sleep(1)
print(f"{name} done")
async def main():
async with anyio.create_task_group() as tg:
tg.start_soon(task, "A")
tg.start_soon(task, "B")
# 指定 Trio 后端运行
anyio.run(main, backend="trio")
# 也可以指定 asyncio 后端
anyio.run(main, backend="asyncio")
AnyIO 提供的核心抽象包括:
anyio.create_task_group()—— 结构化并发任务组(支持 Trio/asyncio 双后端)anyio.sleep()、anyio.run()—— 与标准库 API 一致但可切换后端anyio.open_tcp_stream()、anyio.open_tcp_listener()—— 跨平台网络 I/Oanyio.CancelScope—— 可嵌套、可组合的超时和取消作用域
4. Trio vs asyncio 对比
| 特性 | asyncio | Trio |
|---|---|---|
| 任务创建 | create_task()(无管理) | nursery.start_soon()(结构化) |
| 并发管理 | 手动 gather / wait / TaskGroup | async with nursery 自动管理 |
| 取消语义 | CancelledError 异常,容易遗漏 | CancelScope 精确控制 |
| 超时处理 | wait_for() 超时后任务继续运行 | with trio.move_on_after() 真正取消 |
| 异常传播 | gather(return_exceptions=True) 手动处理 | nursery 自动冒泡,崩溃即全部取消 |
| 锁与信号量 | 标准库 asyncio.Lock() | trio.Lock()(更安全的语义) |
| 生态兼容 | 原生,生态最广 | 需 AnyIO 借用 asyncio 生态 |
| 调试体验 | 一般,堆栈较长 | 结构化后堆栈清晰 |
5. 结构化并发模式
5.1 并行的哨兵模式
在 asyncio 中,实现「谁先完成就选谁」需要手动管理多个 future。Trio 的 trio.move_on_after 配合 nursery 天然支持:
import trio
async def fetch_with_timeout():
with trio.move_on_after(3.0):
async with trio.open_nursery() as nursery:
nursery.start_soon(fetch_cache)
nursery.start_soon(fetch_database)
# 如果两个都成功,取最先完成的由外部逻辑决定
return result
return None # 超时返回默认值
5.2 优雅降级:主从任务
import anyio
async def primary_service():
# 主服务,失败则整体取消
await anyio.sleep(0.1)
return "primary"
async def fallback_service():
# 后备服务,主服务成功时它会被自动取消
await anyio.sleep(5.0)
return "fallback"
async def resilient_fetch():
async with anyio.create_task_group() as tg:
tg.start_soon(primary_service)
tg.start_soon(fallback_service)
# 使用 anyio,任务组内部任一任务异常,其余任务自动取消
6. Trio 的取消与超时
6.1 可组合的取消作用域
Trio 的取消不是异常,而是一个「作用域」:
import trio
async def resilient_operation():
with trio.move_on_after(5.0):
# 5 秒内没完成,自动跳到作用域外
await fetch_slow_data()
# 作用域结束,即使 fetch_slow_data 没完成也不会继续
print("已超时或已完成")
更精细的是 CancelScope,支持主动触发取消而不依赖超时:
async def cancellable_work():
with trio.CancelScope() as scope:
scope.cancel() # 手动触发取消
await trio.sleep(10) # 立刻被中断
6.2 嵌套超时
async def nested_timeouts():
with trio.move_on_after(10.0): # 外层:总超时 10 秒
with trio.move_on_after(3.0): # 内层:单次请求 3 秒
await http_request_1()
with trio.move_on_after(3.0):
await http_request_2()
with trio.move_on_after(3.0):
await http_request_3()
内层超时不影响外层——作用域退出后,父作用域仍然有效。
6.3 屏蔽取消:关键区保护
async def critical_section():
with trio.CancelScope(shield=True):
# 这段代码不会被子任务的取消影响
await save_checkpoint()
shield=True 确保即使在父 nursery 被整体取消时,清理逻辑也能执行完毕。
7. 任务组与异常传播
7.1 Trio 的异常冒泡
在 Trio 的 nursery 中,只要有一个子任务抛出未捕获异常,其余所有子任务会被立即取消,异常冒泡到父作用域。
import trio
async def flaky_worker():
await trio.sleep(0.5)
raise ValueError("模拟故障")
async def slow_worker():
await trio.sleep(10.0)
print("永远不会打印——因为 flaky_worker 崩溃导致被取消")
async def main():
try:
async with trio.open_nursery() as nursery:
nursery.start_soon(flaky_worker)
nursery.start_soon(slow_worker)
except* ValueError as eg:
print(f"捕获异常组: {eg.exceptions}")
trio.run(main)
Trio 使用 ExceptionGroup(PEP 654)合并多任务异常,支持 except* 语法精准捕获。
7.2 AnyIO 的任务组
AnyIO 的 create_task_group() 在 asyncio 后端上模拟了 Trio 的语义:
import anyio
async def division_task():
await anyio.sleep(0.1)
1 / 0 # ZeroDivisionError
async def main():
try:
async with anyio.create_task_group() as tg:
tg.start_soon(division_task)
tg.start_soon(lambda: anyio.sleep(10))
except ExceptionGroup as eg:
for e in eg.exceptions:
print(f"子异常: {type(e).__name__}: {e}")
anyio.run(main, backend="asyncio")
即使运行在 asyncio 后端,AnyIO 也保证了「一个失败、全体取消」的结构化行为。
8. Trio 与 asyncio 世界之间的 Portal
现实项目中,我们无法完全抛弃 asyncio 生态。Trio 提供了 trio_asyncio 库用于在两个世界之间「开传送门」。
pip install trio-asyncio
8.1 在 Trio 中调用 asyncio 代码
import trio
import trio_asyncio
import asyncio
async def legacy_asyncio_code():
# 原始的 asyncio 代码
await asyncio.sleep(1)
return "legacy result"
async def main():
await trio_asyncio.aio_as_trio(legacy_asyncio_code)()
trio_asyncio.run(main)
8.2 反向调用:在 asyncio 中运行 Trio
import asyncio
import trio_asyncio
trio_asyncio.run(trio_main) # 以 Trio 为主循环
# 或在 asyncio 事件循环中嵌入 trio
这种模式允许渐进式迁移——从 asyncio 旧代码逐步替换为 Trio,而无需一次性重写整个项目。
9. 实战示例
9.1 HTTP 并发请求:Trio + httpx
Trio 本身没有内置 HTTP 客户端,但 httpx 通过 AnyIO 支持 Trio 后端:
import trio
import httpx
async def fetch_url(client: httpx.AsyncClient, url: str):
resp = await client.get(url)
return {"url": url, "status": resp.status_code, "len": len(resp.text)}
async def fetch_all(urls: list[str]):
async with httpx.AsyncClient() as client:
async with trio.open_nursery() as nursery:
results = {}
def collect(name, value):
results[name] = value
for url in urls:
nursery.start_soon(
lambda u=url: collect(u, fetch_url(client, u)),
)
return results
urls = [
"https://httpbin.org/get",
"https://httpbin.org/delay/1",
"https://httpbin.org/status/404",
]
# 注意:httpx 本身通过 anyio 支持 trio,因此可以直接在 trio 中运行
# 生产环境更推荐使用 asks 库(原生 Trio HTTP)
更地道的 Trio HTTP 方案是使用 asks:
import asks
import trio
asks.init("trio")
async def fetch_with_asks():
async with trio.open_nursery() as nursery:
async def get(url):
r = await asks.get(url)
print(f"{url}: {r.status_code}")
nursery.start_soon(get, "https://example.com")
nursery.start_soon(get, "https://httpbin.org/get")
9.2 WebSocket 服务器:AnyIO
AnyIO 提供了标准化的 Socket API,以下是基于 AnyIO 的 WebSocket Echo 服务器:
import anyio
import json
async def ws_handler(stream):
"""模拟 WebSocket 帧处理"""
async with anyio.create_task_group() as tg:
async def read_loop():
while True:
data = await stream.receive(4096)
if not data:
break
msg = json.loads(data.decode())
print(f"收到: {msg}")
tg.cancel_scope.cancel() # 示例:收到消息后关闭
tg.start_soon(read_loop)
async def tcp_server():
listener = await anyio.create_tcp_listener(local_port=8765)
print("WebSocket-like server on :8765")
await listener.serve(ws_handler)
anyio.run(tcp_server, backend="trio")
AnyIO 的真正威力在于库的编写者使用它后,终端用户可以自由选择后端。例如 httpcore 和 httpx 都基于 AnyIO,因此自动获得 Trio 支持。
9.3 超时重试与取消
import anyio
async def fetch_with_retry(url: str, retries: int = 3):
for attempt in range(retries):
try:
with anyio.move_on_after(2.0):
# 模拟 HTTP 请求
await anyio.sleep(1.0)
return f"数据 from {url}"
except Exception:
pass
await anyio.sleep(0.5 * (2 ** attempt)) # 指数退避
raise TimeoutError(f"{url} 在 {retries} 次尝试后超时")
async def main():
async with anyio.create_task_group() as tg:
tg.start_soon(fetch_with_retry, "api/users")
tg.start_soon(fetch_with_retry, "api/orders")
tg.start_soon(fetch_with_retry, "api/products")
anyio.run(main, backend="trio")
10. 何时选择 Trio,何时选择 asyncio
选择 Trio 的场景
- 你需要严格的结构化并发保证,任务层级非常重要(如金融交易、工业控制)
- 取消和超时是核心业务逻辑,不能容忍「取消泄漏」
- 项目从零开始,愿意使用 AnyIO 包装后的生态系统
- 团队重视代码正确性胜于立即获得所有第三方库支持
选择 asyncio 的场景
- 你在维护已有的大规模 asyncio 代码库,迁移成本过高
- 依赖的库(如
asyncpg、aioredis早期版本)没有 AnyIO/Trio 适配 - Python 3.11 的
TaskGroup和ExceptionGroup已满足部分结构化需求 - 团队对 asyncio 有深厚积累,无需额外学习成本
折中方案:AnyIO
对于新项目,最佳实践是用 AnyIO 编写业务代码,保留后端切换的灵活性:
- 开发测试期使用 Trio 后端,享受更强的调试和正确性保证
- 生产环境若发现某些依赖不兼容,可在
anyio.run()一行参数切换为 asyncio - 开源库的开发者使用 AnyIO 编写,让用户自己选择后端
小结
Trio 为 Python 异步编程带来了结构化并发的全新范式,通过 Nursery 和 CancelScope 解决了 asyncio 长期存在的任务泄漏、取消脆弱等痛点。AnyIO 则作为兼容层,让 Trio 的优雅设计得以在 asyncio 主导的生态中落地。
随着 Python 3.11 引入 TaskGroup 和 ExceptionGroup,标准库正在向结构化并发靠拢,但 Trio 在语义完整性和 API 简洁性上依然领先。理解 Trio 的设计哲学,无论最终选择哪个框架,都会让你写出更可靠、更易维护的异步代码。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。