同步 vs 异步:心智模型
# 同步:等
def fetch(url):
response = http_get(url) # 卡 1 秒
return response
# 异步:让出 CPU
async def fetch(url):
response = await http_get(url) # 等待时让 CPU 去干别的
return response
await 处会让出控制权给事件循环,事件循环安排别的协程跑——等 I/O 回来再继续。
第一个程序
import asyncio
async def hello():
print("hi")
await asyncio.sleep(1)
print("bye")
asyncio.run(hello())
# hi
# (停 1 秒)
# bye
asyncio.run() 是入口——启动事件循环、跑 coroutine、退出。
并发执行:gather
async def task(name, sec):
print(f"{name} 开始")
await asyncio.sleep(sec)
print(f"{name} 结束")
return name
async def main():
results = await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 3),
)
print(results)
asyncio.run(main())
# A 开始
# B 开始
# C 开始
# B 结束 (B 最快)
# A 结束
# C 结束
# ['A', 'B', 'C'] (顺序保留)
# 总耗时 3 秒,不是 2+1+3=6
gather 等所有任务完成。
实战:并发 HTTP 请求
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as r:
return await r.text()
async def main():
urls = ["https://example.com" for _ in range(10)]
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, u) for u in urls]
results = await asyncio.gather(*tasks)
print(f"拿到 {len(results)} 个响应")
asyncio.run(main())
10 个请求并发——总耗时约等于最慢那个。
超时与异常
try:
result = await asyncio.wait_for(fetch(session, url), timeout=5)
except asyncio.TimeoutError:
print("超时")
as_completed:哪个先回先处理
for coro in asyncio.as_completed(tasks):
result = await coro
print(result) # 按完成顺序而不是提交顺序
TaskGroup(Python 3.11+,新版推荐)
async def main():
async with asyncio.TaskGroup() as tg:
a = tg.create_task(task("A", 2))
b = tg.create_task(task("B", 1))
c = tg.create_task(task("C", 3))
# 出 with 块时所有任务都已完成
print(a.result(), b.result(), c.result())
比 gather 更现代——异常处理更好(一个挂了其他能正常取消)。
别在 async 函数里阻塞
async def bad():
time.sleep(1) # ❌ 阻塞整个事件循环 1 秒
requests.get(url) # ❌ 同上
async def good():
await asyncio.sleep(1) # ✓
async with aiohttp.ClientSession() as s:
await s.get(url) # ✓
CPU 密集计算也是阻塞——要么用 loop.run_in_executor 丢到线程池,要么放进进程池。
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, cpu_heavy, arg)
何时不该用 asyncio
- 简单脚本,没几个 I/O:直接同步代码
- CPU 密集:用进程池
- 现有同步代码改造:成本高,先用线程池
- 团队不熟:心智负担重
asyncio 的"传染性"
一旦一个函数是 async,调用它的函数也得是 async(要么 await 它)。这叫**"red function"问题**——你的代码会逐渐被染成红色。
async def a(): ...
async def b(): await a()
async def c(): await b() # 一路 async 上来
下一篇用 aiohttp 写一个真实并发爬虫。