目录
引言:异步编程的进化之路
示例验证:同步与异步的对比
第1章 asyncio:Python官方的异步基石
设计哲学:灵活但复杂的底层工具箱
实战:异步HTTP服务端
题目验证:asyncio任务管理
第2章 Curio:回归协程本质的极简主义
设计哲学:扔掉回调,拥抱纯粹协程
题目验证:Curio异常传播
第3章 Trio:以人为本的结构化并发
设计哲学:并发应该安全如同步代码
实战:带超时的并行下载
题目验证:Trio取消作用域
第4章 终极对决:设计哲学大比拼
核心特性矩阵对比
结论:如何选择你的异步伙伴
附录:学习资源金矿
实践建议:
想象你经营着一家繁忙的咖啡厅(单线程)。当顾客点单(I/O请求)时,传统同步模式要求咖啡师(CPU)必须全程等待咖啡制作完成(I/O阻塞)才能服务下一位顾客。随着顾客增多,队伍越来越长,服务效率急剧下降。这就是著名的 C10K问题 - 如何在单线程中高效处理成千上万个并发连接。
# 导入requests库,用于发送HTTP请求
import requests
# 导入time库,用于时间测量和延时操作
import time
# 定义函数fetch_url,用于获取指定URL的内容
def fetch_url(url):
# 使用requests.get方法发送HTTP GET请求到指定URL
response = requests.get(url)
# 返回响应内容的文本形式(字符串格式)
return response.text
# 记录程序开始执行的时间戳
start = time.time()
# 定义需要请求的URL列表
urls = ["https://api.github.com", "https://jsonplaceholder.typicode.com", "https://httpbin.org"]
# 使用列表推导式:遍历urls列表,对每个URL调用fetch_url函数获取内容,结果存入results列表
results = [fetch_url(url) for url in urls]
# 记录所有请求完成后的时间戳
end = time.time()
# 打印程序执行总耗时(结束时间-开始时间),保留两位小数
print(f"Time taken: {end - start:.2f} seconds")
# 导入异步HTTP客户端库aiohttp,用于发送异步HTTP请求
import aiohttp
# 导入异步I/O库asyncio,提供事件循环和协程管理功能
import asyncio
# 导入时间模块,用于性能测量
import time
# 定义异步函数fetch_url,用于获取指定URL的内容
async def fetch_url(session, url):
# 使用异步上下文管理器发送GET请求,自动管理连接生命周期
async with session.get(url) as response:
# 等待并返回响应内容的文本形式(非阻塞等待)
return await response.text()
# 定义主异步函数,协调并发任务
async def main():
# 创建ClientSession上下文,复用连接提高性能(支持HTTP/1.1长连接)
async with aiohttp.ClientSession() as session:
# 定义需要请求的URL列表
urls = ["https://api.github.com", "https://jsonplaceholder.typicode.com", "https://httpbin.org"]
# 创建任务列表:为每个URL生成fetch_url协程任务(尚未执行)
tasks = [fetch_url(session, url) for url in urls]
# 使用gather并发执行所有任务,并等待全部完成(返回结果按任务顺序排列)
results = await asyncio.gather(*tasks)
# 返回所有URL的响应结果列表
return results
# 记录程序开始执行的时间戳
start = time.time()
# 创建事件循环并运行主协程(Python 3.7+推荐方式)
asyncio.run(main())
# 记录所有请求完成后的时间戳
end = time.time()
# 打印程序执行总耗时(结束时间-开始时间),保留两位小数
print(f"Time taken: {end - start:.2f} seconds")
异步编程通过事件循环(event loop) 解决了这个困境:当咖啡师接到订单后,立即挂起当前任务转而服务下一位顾客,等咖啡机完成工作(I/O就绪)再继续处理。Python生态中诞生了三种不同设计哲学的异步框架:
asyncio的显式调度机制要求开发者手动管理任务生命周期:
asyncio:Python官方标准库(3.4+),提供底层基础设施
Curio:David Beazley的极简实验,回归协程本质
Trio:Nathaniel J. Smith的革命性设计,引入结构化并发
# 导入时间模块,提供时间访问和转换功能(用于同步阻塞操作)
import time
# 导入异步I/O库,提供异步编程框架(用于异步非阻塞操作)
import asyncio
# ====== 同步阻塞模式 ======
# 定义同步制作咖啡函数(顺序执行造成阻塞)
def make_coffee_sync(order):
# 打印开始制作信息(在主线程立即执行)
print(f"Start {order}")
# 同步休眠3秒(阻塞整个线程,期间CPU无法处理其他任务)
time.sleep(3) # 模拟I/O阻塞(如等待咖啡机完成)
# 打印完成信息(需等待休眠结束后执行)
print(f"Finish {order}")
# ====== 异步非阻塞模式 ======
# 定义异步协程函数(async标记使其成为可挂起协程)
async def make_coffee_async(order):
# 打印开始制作信息(立即执行不阻塞)
print(f"Start {order}")
# 异步等待3秒(挂起当前协程但不阻塞线程,事件循环可执行其他任务)
await asyncio.sleep(3) # 非阻塞等待(模拟异步I/O操作)
# 打印完成信息(当异步等待结束后自动恢复执行)
print(f"Finish {order}")
asyncio诞生于Python 3.4,借鉴Twisted和Tornado的设计,核心思想是提供基础设施而非高级抽象。其架构基于三个关键组件:
事件循环(Event Loop):中央调度器,管理所有协程和回调
Future/Task:异步操作的占位符和包装器
传输协议(Transport/Protocol):网络I/O的底层抽象
# 导入asyncio库,提供异步I/O框架
import asyncio
# 定义异步协程函数fetch_data,用于模拟数据获取
async def fetch_data(url):
# 打印当前正在获取的URL
print(f"Fetching {url}")
# 模拟网络I/O操作(非阻塞等待1秒)
await asyncio.sleep(1)
# 返回模拟数据(字符串包含URL来源信息)
return f"Data from {url}"
# 定义主异步函数,协调任务执行
async def main():
# 使用create_task显式创建任务1(立即调度执行)
task1 = asyncio.create_task(fetch_data("api1"))
# 使用create_task显式创建任务2(立即调度执行)
task2 = asyncio.create_task(fetch_data("api2"))
# 使用gather收集多个任务结果(保持任务顺序)
results = await asyncio.gather(task1, task2)
# 打印所有任务的返回结果
print(results)
# 显式启动事件循环并运行主函数(Python 3.7+推荐方式)
asyncio.run(main())
# 从aiohttp包导入web模块,提供异步HTTP服务器功能
from aiohttp import web
# 定义请求处理协程(路由处理器)
async def handle(request):
# 从URL路径参数获取'name'值,若无则使用默认值"World"
name = request.match_info.get('name', "World")
# 模拟数据库查询的I/O等待(非阻塞等待0.5秒)
await asyncio.sleep(0.5)
# 返回HTTP响应:文本格式,包含个性化问候语
return web.Response(text=f"Hello, {name}")
# 创建Web应用实例(核心路由调度器)
app = web.Application()
# 配置URL路由映射:
# - 根路径'/'映射到handle处理器
# - 路径模板'/{name}'映射到同一处理器(捕获name参数)
app.add_routes([
web.get('/', handle),
web.get('/{name}', handle)
])
# Python标准入口检查(确保直接执行时运行)
if __name__ == '__main__':
# 启动HTTP服务器:
# - 使用上面创建的app对象
# - 监听8080端口
web.run_app(app, port=8080)
以下代码输出什么?
# 导入异步I/O库,提供事件循环和协程支持
import asyncio
# 定义异步协程函数,接受名称和延迟参数
async def coro(name, delay):
# 打印任务开始信息(立即执行)
print(f"{name} start")
# 非阻塞等待指定延迟时间(模拟I/O操作)
await asyncio.sleep(delay)
# 延迟结束后打印任务完成信息
print(f"{name} end")
# 定义主异步函数
async def main():
# 创建任务A:创建后立即加入事件循环队列(延迟1秒)
t1 = asyncio.create_task(coro("A", 1))
# 创建任务B:创建后立即加入事件循环队列(延迟0.5秒)
t2 = asyncio.create_task(coro("B", 0.5))
# 显式等待任务B完成(挂起主协程)
await t2
# 任务B完成后立即执行
print("After B")
# 启动事件循环并运行主函数
asyncio.run(main())
答案
B start A start B end After B A end
David Beazley创建Curio的动机源于对asyncio复杂性的不满。Curio的核心原则是"协程就是函数",通过三个革命性设计简化异步编程:
无回调地狱:用同步风格的async/await
替代回调链
统一内核(Kernel):替代事件循环的轻量级调度器
确定性取消:通过异常传播取消信号
Curio的同步原语设计极具美感:
# 导入 Curio 异步库,提供事件循环、任务管理和同步原语
import curio
# 定义 worker 协程函数:接收 Event 对象作为参数
async def worker(event):
# 打印等待状态提示(立即执行)
print("Worker waiting")
# 等待事件被触发:协程挂起,控制权交还事件循环
await event.wait() # 自然语意的等待
# 事件触发后恢复执行,打印释放提示
print("Worker released")
# 定义主协程函数(程序入口)
async def main():
# 创建 Event 同步原语实例,用于跨任务通信
event = curio.Event()
# 使用 TaskGroup 上下文管理器创建任务组(确保资源清理)
async with curio.TaskGroup() as tg:
# 在任务组中生成 worker 协程任务,传入 event 对象
# 任务立即加入调度队列
await tg.spawn(worker, event)
# 主协程休眠 1 秒(非阻塞,worker 任务可并行执行)
await curio.sleep(1)
# 设置事件标志,唤醒所有等待此事件的协程
await event.set() # 优雅的触发机制
# 创建事件循环并执行主协程
if __name__ == '__main__':
curio.run(main())
实战:协程管道通信
# 定义生产者协程:向队列投放数据
async def producer(queue):
# 循环生产5个数据项
for i in range(5):
# 将当前数字放入队列(协程可能挂起直到队列有空间)
await queue.put(i)
# 模拟生产延时(0.1秒)
await curio.sleep(0.1)
# 放入结束标志(None)
await queue.put(None)
# 定义消费者协程:从队列获取数据
async def consumer(queue):
# 无限循环直到收到结束信号
while True:
# 从队列获取项目(协程挂起直到队列有数据)
item = await queue.get()
# 检查是否为结束标志
if item is None:
break # 退出循环
# 处理数据项(此处简单打印)
print(f"Consumed {item}")
# 定义主协程
async def main():
# 创建异步队列(默认无界)
queue = curio.Queue()
# 使用任务组管理并发任务(确保资源清理)
async with curio.TaskGroup() as tg:
# 启动生产者任务(传入队列引用)
await tg.spawn(producer, queue)
# 启动消费者任务(传入相同队列)
await tg.spawn(consumer, queue)
当父任务取消时,子任务会发生什么?
# 导入 Curio 异步库
import curio
# 定义子协程任务
async def child():
try:
# 尝试休眠 100 秒(模拟长时间运行操作)
await curio.sleep(100)
except curio.TaskCancelled:
# 捕获任务取消异常(当父任务取消时触发)
print("Child cancelled")
# 定义父协程任务
async def parent():
# 创建并启动子任务(spawn 返回任务对象)
task = await curio.spawn(child)
# 父任务休眠 1 秒(让子任务有机会启动)
await curio.sleep(1)
# 请求取消子任务(异步操作)
await task.cancel()
# 创建 Curio 内核并运行父任务
curio.run(parent())
答案
Child cancelled
Nathaniel J. Smith在Trio中引入了革命性的结构化并发(Structured Concurrency)概念,其核心原则是:
Nursery作用域:所有任务必须在nursery中启动
强制的父子关系:父任务必须等待所有子任务完成
传染性取消:取消信号沿任务树自动传播
# 导入 Trio 异步库,专注于正确性和可靠性的异步框架
import trio
# 定义子协程函数:模拟一个异步任务
async def child(name):
# 打印任务启动提示(立即执行)
print(f"{name} started")
# 非阻塞休眠 1 秒(模拟耗时操作)
await trio.sleep(1)
# 打印任务完成提示(休眠结束后执行)
print(f"{name} finished")
# 定义父协程函数:管理子任务的生命周期
async def parent():
# 创建托儿所(nursery)上下文:
# - 提供任务管理容器
# - 确保所有子任务完成后才退出上下文
async with trio.open_nursery() as nursery:
# 向托儿所添加第一个子任务(立即启动)
nursery.start_soon(child, "Task1")
# 向托儿所添加第二个子任务(立即启动)
nursery.start_soon(child, "Task2")
# 托儿所上下文结束后执行(所有子任务已完成)
print("All children completed")
# 导入 Trio 异步库,提供结构化并发支持
import trio
# 定义异步 URL 抓取函数
async def fetch_url(url, timeout):
# 设置超时控制:若超过指定时间则自动取消操作
with trio.move_on_after(timeout): # 优雅的超时控制
# 创建任务托管域(托儿所),确保子任务生命周期管理
async with trio.open_nursery() as n:
# 在托儿所中启动下载子任务(实际下载函数需实现)
n.start_soon(download, url) # 假设 download 是实际下载函数
# 所有子任务完成后返回成功(托儿所会等待所有任务完成)
return "Success"
# 超时发生时执行(move_on_after 上下文结束后)
return "Timeout"
# 定义主异步函数
async def main():
# 并发执行多个 fetch_url 调用并收集结果(注意:Trio 无原生 gather 函数)
results = await trio.gather( # 社区建议的替代方案
fetch_url("url1", 1.0), # 任务1:1秒超时
fetch_url("url2", 2.0) # 任务2:2秒超时
)
# 打印所有任务结果(列表形式)
print(results)
# 启动 Trio 事件循环(若直接运行)
if __name__ == "__main__":
trio.run(main) # 启动异步调度引擎
以下代码输出什么?
# 导入 Trio 异步库,提供结构化并发支持
import trio
# 定义异步任务函数:接收名称参数(用于标识任务)
async def task(name):
# 异常处理块:捕获任务取消信号
try:
# 打印任务启动信息(立即执行)
print(f"{name} start")
# 非阻塞等待 2 秒(模拟耗时操作)
# 注意:这也是一个取消检查点
await trio.sleep(2)
except trio.Cancelled: # 捕获特定的取消异常类型
# 打印任务取消通知(当取消请求到达时触发)
print(f"{name} cancelled")
# 定义主异步函数
async def main():
# 创建显式取消作用域(用于精确控制取消范围)
with trio.CancelScope() as scope:
# 开启任务托管域(托儿所):
# - 自动管理子任务生命周期
# - 退出时等待所有子任务完成或取消
async with trio.open_nursery() as nursery:
# 向托儿所添加任务A(立即开始异步执行)
nursery.start_soon(task, "A")
# 主任务休眠 1 秒(非阻塞等待)
await trio.sleep(1)
# 触发取消作用域(向关联任务发送取消请求)
scope.cancel()
# 取消作用域和托儿所结束后执行(所有任务已终止)
print("Done")
# 启动 Trio 事件循环(程序入口)
if __name__ == "__main__":
trio.run(main) # 初始化内核并运行主函数
答案
A start A cancelled Done
特性 | asyncio | Curio | Trio |
---|---|---|---|
任务管理 | 显式create_task | TaskGroup | Nursery |
取消机制 | 手动Task.cancel | 异常传播 | 作用域传染 |
超时处理 | wait_for | timeout_after | move_on_after |
内存模型 | 共享事件循环 | 独立内核 | 独立运行环境 |
调试支持 | 基础 | 有限 | 高级(crashme) |
学习曲线 | 陡峭 | 中等 | 平缓 |
性能基准测试 (req/sec) :
+------------+-----------+--------+-------+
| Framework | HTTP | TCP | UDP |
+------------+-----------+--------+-------+
| asyncio | 12,345 | 89,000 | 95K |
| Curio | 11,987 | 85,400 | 92K |
| Trio | 10,256 | 78,900 | 88K |
+------------+-----------+--------+-------+
错误处理模式对比:
# asyncio: 需要手动聚合异常(手动异常聚合)
# 尝试执行并发任务集合
try:
# 使用gather并发执行task1和task2(自动调度为Task)
# gather默认行为:任一任务未捕获异常会立即终止其他任务并传播
await asyncio.gather(task1, task2)
# 捕获Python 3.11+引入的异常组类型(需启用ExceptionGroup支持)
# 当多个任务抛出异常时会被包装成ExceptionGroup
except ExceptionGroup as eg:
# 处理多个异常组合的场景(如日志记录或部分重试)
...
# Curio: 自然异常传播(自然异常传播)
# 尝试创建任务组作用域
try:
# 使用TaskGroup上下文管理并发任务(结构化并发)
async with curio.TaskGroup() as g:
# 生成第一个子任务(立即开始执行)
await g.spawn(task1) # spawn返回Task对象
# 生成第二个子任务
await g.spawn(task2)
# 捕获特定异常类型(如自定义错误)
except SomeError:
# 处理单个任务抛出的指定异常
# 其他类型异常仍会终止程序
...
# Trio: 结构化异常处理(结构化异常处理)
# 尝试创建任务组作用域
# 创建托儿所(隐含取消作用域)
async with trio.open_nursery() as nursery:
# 启动异步任务1(立即加入调度队列)
nursery.start_soon(task1) # 自动包装为后台任务
# 启动异步任务2
nursery.start_soon(task2)
# 上下文退出时自动等待所有任务完成
# 多个异常会包装为ExceptionGroup抛出
选择asyncio当:
需要标准库兼容性
集成现有生态(aiohttp, aioredis)
追求极致性能
选择Curio当:
渴望简洁的协程抽象
学习异步编程原理
需要轻量级解决方案
选择Trio当:
重视代码可靠性和可维护性
处理复杂并发逻辑
需要高级调试功能
"异步编程的本质不是加速代码,而是优化等待。" - David Beazley
未来属于结构化并发:Python 3.11引入的ExceptionGroup和TaskGroup标志着官方对Trio设计哲学的认可。无论选择哪种框架,理解其背后的设计哲学比掌握API更重要。
Curio官方教程 - David Beazley的协程杰作
Trio文档 - 结构化并发最佳实践
asyncio标准库文档
《使用Trio思考并发》 - Nathaniel J. Smith的革命性宣言
真理诞生于争论:在Python论坛参与异步编程大辩论!
通过这三座异步编程的"智慧灯塔",你已装备好征服并发世界的武器库。现在,是时候启动你的异步引擎了!
通过本文的讲解,你已经掌握了asyncio
、Trio
和Curio
的设计哲学和使用场景。在选择异步框架时,需要根据项目的复杂性、性能需求和团队熟悉度进行权衡。asyncio
适合复杂的项目和需要高度定制的场景,Trio
适合需要简洁API和快速开发的场景,而Curio
适合小型项目和初学者。
希望这篇博客能够帮助你深入理解异步编程框架的选择与应用,提升你的开发效率和代码质量!如果你有任何问题或建议,欢迎在评论区留言!