同步 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 写一个真实并发爬虫。