Python 异步编程完全指南:async/await 深入剖析
Python 的异步编程经历了从回调地狱到 async/await 的演进。本文带你深入理解 Python 的异步模型。
为什么需要异步?
传统的同步 I/O 会阻塞线程:
import time
def fetch_url(url):
time.sleep(1) # 模拟网络请求
return f"Data from {url}"
def main():
urls = ["url1", "url2", "url3"]
results = [fetch_url(url) for url in urls]
# 需要 3 秒!
异步版本可以在等待 I/O 时切换到其他任务:
import asyncio
async def fetch_url(url):
await asyncio.sleep(1)
return f"Data from {url}"
async def main():
urls = ["url1", "url2", "url3"]
tasks = [fetch_url(url) for url in urls]
results = await asyncio.gather(*tasks)
# 只需要约 1 秒!
协程(Coroutine)
async def 定义的是协程函数,调用它返回一个协程对象:
async def hello():
return "Hello"
coro = hello()
print(type(coro)) # <class 'coroutine'>
# 协程需要通过 await 或 asyncio.run() 来执行
result = asyncio.run(coro)
print(result) # Hello
事件循环(Event Loop)
事件循环是异步编程的核心——它负责调度和执行协程:
import asyncio
async def task(name, delay):
await asyncio.sleep(delay)
print(f"Task {name} done")
return name
async def main():
# 同时启动三个任务
results = await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 0.5),
)
print(f"Results: {results}")
asyncio.run(main())
# 输出顺序:C → B → A → Results: ['A', 'B', 'C']
Task 与 Future
- Task:对协程的封装,调度其在事件循环中执行
- Future:表示一个尚未完成的结果
async def main():
# create_task 立即调度协程
task1 = asyncio.create_task(fetch_url("url1"))
task2 = asyncio.create_task(fetch_url("url2"))
# 可以取消任务
task2.cancel()
try:
await task2
except asyncio.CancelledError:
print("Task 2 was cancelled")
result = await task1
print(result)
异步上下文管理器
import asyncio
class AsyncConnection:
async def __aenter__(self):
print("Opening connection...")
await asyncio.sleep(0.1)
return self
async def __aexit__(self, *args):
print("Closing connection...")
await asyncio.sleep(0.1)
async def query(self, sql):
await asyncio.sleep(0.1)
return f"Result of: {sql}"
async def main():
async with AsyncConnection() as conn:
result = await conn.query("SELECT * FROM users")
print(result)
异步迭代器
class AsyncRange:
def __init__(self, start, end):
self.current = start
self.end = end
def __aiter__(self):
return self
async def __anext__(self):
if self.current >= self.end:
raise StopAsyncIteration
await asyncio.sleep(0.1) # 模拟异步操作
self.current += 1
return self.current - 1
async def main():
async for num in AsyncRange(0, 5):
print(num)
异步生成器
async def async_range(start, end):
for i in range(start, end):
await asyncio.sleep(0.1)
yield i
async def main():
async for num in async_range(0, 5):
print(num)
# 异步列表推导(Python 3.11+)
results = [num async for num in async_range(0, 5)]
print(results) # [0, 1, 2, 3, 4]
实战:异步 HTTP 客户端
import asyncio
import aiohttp
from typing import List, Dict
class AsyncHttpClient:
def __init__(self, concurrency: int = 10):
self.semaphore = asyncio.Semaphore(concurrency)
self.session: aiohttp.ClientSession | None = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, *args):
if self.session:
await self.session.close()
async def fetch(self, url: str) -> Dict:
async with self.semaphore:
async with self.session.get(url) as response:
return {
"url": url,
"status": response.status,
"body": await response.text()[:200]
}
async def fetch_many(self, urls: List[str]) -> List[Dict]:
tasks = [self.fetch(url) for url in urls]
return await asyncio.gather(*tasks, return_exceptions=True)
async def main():
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/get",
] * 5 # 15 个请求
async with AsyncHttpClient(concurrency=5) as client:
results = await client.fetch_many(urls)
for r in results:
if isinstance(r, Exception):
print(f"Error: {r}")
else:
print(f"{r['url']}: {r['status']}")
asyncio.run(main())
常见陷阱
1. 在协程中调用阻塞函数
# ❌ 错误:阻塞事件循环
async def bad():
time.sleep(5) # 阻塞整个事件循环!
# ✅ 正确:使用线程池
async def good():
await asyncio.to_thread(time.sleep, 5)
2. 忘记 await
async def fetch_data():
await asyncio.sleep(1)
return "data"
async def main():
# ❌ coroutine was never awaited
result = fetch_data()
# ✅
result = await fetch_data()
3. 在同步代码中使用 asyncio.run()
# ❌ 在已有事件循环的环境中使用 asyncio.run()
# asyncio.run(fetch_data())
# ✅ 在 Jupyter 等环境中使用
await fetch_data()
总结
Python 的异步编程模型相对直观,但需要注意:
- 使用
async/await编写协程 - 用
asyncio.gather()并发执行多个任务 - 避免在协程中调用阻塞函数
- 使用
aiohttp、asyncpg等异步库获得真正的性能提升 - 理解事件循环的工作方式帮助调试问题