前言
在I/O密集型应用中,Python的异步编程(asyncio)可以显著提升性能。然而,许多开发者对asyncio的理解停留在表面,导致代码效率低下或出现难以调试的问题。本文将从事件循环、协程、任务和Future出发,深入剖析asyncio的工作原理,并通过实战案例展示最佳实践。
1. 事件循环:异步的引擎
事件循环是asyncio的核心,它负责调度和执行协程。Python 3.10+推荐使用asyncio.run()来启动事件循环。
import asyncio
async def main():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(main())
注意:asyncio.run()会创建新的事件循环,并在完成后关闭它。不要手动调用loop.close(),除非你有特殊需求。
底层事件循环操作
如果需要更精细的控制,可以使用loop = asyncio.neweventloop()和loop.rununtilcomplete(),但通常不推荐。
2. 协程:async/await语法
协程是通过async def定义的函数。调用协程函数不会立即执行,而是返回一个协程对象。
async def fetch_data():
print("开始获取数据")
await asyncio.sleep(2)
print("数据获取完成")
return {"data": 42}
# 正确执行
result = asyncio.run(fetch_data())
print(result) # 输出: {'data': 42}
常见错误:忘记await,导致协程对象未被执行。
# 错误示例
async def main():
fetch_data() # 协程对象被创建但未执行
asyncio.run(main())
3. 任务:并发执行协程
任务是协程的封装,用于并发执行。使用asyncio.create_task()创建任务。
async def task1():
await asyncio.sleep(2)
return "Task1 done"
async def task2():
await asyncio.sleep(1)
return "Task2 done"
async def main():
t1 = asyncio.create_task(task1())
t2 = asyncio.create_task(task2())
# 等待所有任务完成
results = await asyncio.gather(t1, t2)
print(results)
asyncio.run(main())
输出:['Task1 done', 'Task2 done'],总耗时约2秒(并发执行)。
任务取消与超时
使用asyncio.wait_for()设置超时,或task.cancel()取消任务。
async def slow_operation():
await asyncio.sleep(10)
return "完成"
async def main():
try:
result = await asyncio.wait_for(slow_operation(), timeout=3)
except asyncio.TimeoutError:
print("操作超时")
asyncio.run(main())
注意:超时后任务会被取消,并抛出CancelledError。如果需要清理资源,可以捕获CancelledError。
4. Future:底层异步结果
Future是表示异步操作结果的类,类似于JavaScript的Promise。通常我们不直接使用Future,而是通过ensurefuture()或loop.createfuture()创建。
async def set_future(fut):
await asyncio.sleep(1)
fut.set_result("Future resolved")
async def main():
loop = asyncio.get_running_loop()
fut = loop.create_future()
asyncio.create_task(set_future(fut))
result = await fut
print(result)
asyncio.run(main())
理解:await fut会挂起当前协程,直到Future被设置结果。
5. 实战:异步HTTP请求
使用aiohttp库进行异步HTTP请求(需安装:pip install aiohttp)。
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/3"]
async with aiohttp.ClientSession() as session:
tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
pages = await asyncio.gather(*tasks)
print(f"获取了 {len(pages)} 个页面")
asyncio.run(main())
性能对比:同步版本需要约6秒,异步版本仅需约3秒(取决于网络延迟)。
6. 常见陷阱与最佳实践
陷阱1:阻塞事件循环
在协程中不要使用time.sleep(),它会阻塞整个事件循环。使用asyncio.sleep()。
# 错误
async def bad():
time.sleep(1) # 阻塞事件循环
# 正确
async def good():
await asyncio.sleep(1)
陷阱2:忘记await
确保所有协程都被await或作为任务调度。
陷阱3:共享状态
异步代码中共享可变状态可能导致竞态条件。使用asyncio.Lock保护临界区。
lock = asyncio.Lock()
async def critical_section():
async with lock:
# 访问共享资源
pass
最佳实践:使用asyncio.run()
始终使用asyncio.run()作为入口,它负责创建和关闭事件循环。
最佳实践:合理使用gather与wait
asyncio.gather():返回所有结果,如果某个任务异常,会立即抛出。asyncio.wait():更灵活,可以等待所有、第一个完成、第一个异常等。
7. 总结
本文介绍了asyncio的核心概念:事件循环、协程、任务和Future,并通过实战案例展示了异步HTTP请求。关键要点:
- 使用
async/await定义协程 - 用
asyncio.create_task()并发执行 - 避免阻塞事件循环
- 使用
asyncio.run()管理事件循环
延伸阅读:
- asyncio官方文档:https://docs.python.org/zh-cn/3/library/asyncio.html
- 异步编程模式:回调、Future、协程对比
希望本文能帮助你深入理解asyncio,写出高效可靠的异步Python代码。