⚡ async/await 是什么 怎么用 事件循环 对比 坑点 总结

Python async / await 完全指南

一次性彻底搞懂异步编程:从"为什么需要"到"怎么用"到"底层原理"

一、async / await 到底是什么?

先把最核心的一句话记住:async/await 是 Python 实现「协程(Coroutine)」的语法糖,用来写「异步非阻塞」代码。

类比:咖啡店点餐

同步(普通函数):你点一杯咖啡,站在柜台前一直等,店员做不完你不走,后面的人也只能排队。——浪费时间!

异步(async/await):你点完咖啡,拿个小票先去坐着,店员做完叫你。期间店员还可以给其他人做咖啡。——效率拉满!
async def

声明协程函数

在函数定义前加 async,这个函数就变成了一个协程函数。调用它不会立即执行,而是返回一个协程对象(需要事件循环来驱动执行)。

await

挂起等待结果

在协程函数内部,用 await 等待一个可等待对象(协程 / Task / Future)。等待期间,控制权交还给事件循环,可以去执行其他协程。

asyncio.run()

启动事件循环

Python 3.7+ 的入口函数。创建一个事件循环,运行传入的协程,直到它完成,然后关闭事件循环。你现在只需要记住:异步代码从这里开始

awaitable

可等待对象

能用 await 等待的对象,包括:协程(coroutine)TaskFuture。普通函数返回值不能用 await!

二、怎么用?从最简到进阶

下面所有代码都可以直接复制运行(Python 3.7+)。

① 最基础:定义 + 调用

Python
# 定义一个协程函数(不会马上执行!)
async def say_hello():
    print("Hello")
    await asyncio.sleep(1)  # 模拟IO等待1秒
    print("World")
    return "done"

# 正确调用方式:用 asyncio.run()
import asyncio
result = asyncio.run(say_hello())
print(result)  # 输出: done

② 核心优势:并发执行(同时跑多个协程)

Python
import asyncio, time

# 模拟一个网络请求
async def fetch_data(name, seconds):
    print(f"{name} 开始请求...")
    await asyncio.sleep(seconds)  # 模拟IO等待
    print(f"{name} 请求完成!")
    return f"{name}的数据"

async def main():
    start = time.time()

    # ✅ 并发执行:三个请求同时发出
    # gather 会同时启动所有协程,总耗时 ≈ 最慢的那个
    results = await asyncio.gather(
        fetch_data("请求A", 2),
        fetch_data("请求B", 3),
        fetch_data("请求C", 1),
    )

    print(f"结果: {results}")
    print(f"总耗时: {time.time()-start:.2f}秒")
    # 输出: 总耗时 ≈ 3秒(不是 2+3+1=6秒!)

asyncio.run(main())

③ 进阶:asyncio.create_task(更灵活地管理协程)

Python
async def main():
    # create_task: 把协程包装成 Task,立即加入事件循环调度
    # 不像 await 会阻塞,create_task 马上返回
    task1 = asyncio.create_task(fetch_data("A", 2))
    task2 = asyncio.create_task("B", 3))

    # 这里可以做其他事情...
    print("任务已提交,做点别的...")

    # 需要结果时再 await
    result1 = await task1
    result2 = await task2
    print(f"结果: {result1}, {result2}")

④ 常见错误:忘了 await

Python
# ❌ 错误:直接调用协程函数,啥也不会发生!
say_hello()  # 只是返回了一个协程对象,没有执行!
# 输出: RuntimeWarning: coroutine was never awaited

# ❌ 错误:用普通方式调用(不是异步环境)
result = say_hello()  # 得到的是 coroutine 对象,不是结果!

# ✅ 正确:用 await 或 asyncio.run()
asyncio.run(say_hello())   # 方式1:作为入口
result = await say_hello()  # 方式2:在协程内部
三、事件循环(Event Loop)动画演示

事件循环是异步的核心引擎。下面用可视化动画展示它的工作原理。

💡 事件循环的核心逻辑(记住这4步)

  1. 执行:从任务队列取出一个协程执行
  2. 遇到 await:挂起当前协程,把它放到"等待区"
  3. 切换:事件循环去执行其他就绪的协程
  4. 恢复:等待的操作完成后,协程重新进入"就绪队列",继续往下执行
四、同步 vs 异步 vs 多线程 对比
对比维度同步(普通函数)异步(async/await)多线程(threading)
执行方式顺序执行,一行等一行协程协作式,await 时让出控制权操作系统抢占式,真正并行(多核)
适用场景CPU 密集型(计算多)IO 密集型(网络/文件)IO 密集型 + 部分 CPU 密集型
开销极低(协程是用户态,切换快)较高(线程切换有系统开销)
并发量1个数万个协程无压力几百个线程就吃力
代码复杂度最简单中等(要理解 await 机制)高(要考虑锁、竞争条件)
GIL 影响受 GIL 限制,多核无力单线程,不受 GIL 影响(但也不能利用多核)受 GIL 限制(IO 时可释放 GIL)
🧵
一句话总结三者选择:
计算多(视频编码、数据分析):用 多进程(multiprocessing)
IO 多(爬虫、API 调用、Web 服务):用 异步(async/await)
既要 IO 又要简单:用 多线程(threading)(但要小心锁)
五、常见坑点(避坑指南)

❌ 坑点1:在 async 函数里调用同步阻塞代码

Python
# ❌ 错误:time.sleep() 会阻塞整个事件循环!
async def bad():
    time.sleep(1)  # 整个程序卡住1秒,其他协程也无法执行!

# ✅ 正确:用 asyncio.sleep()
async def good():
    await asyncio.sleep(1)  # 只挂起当前协程,其他协程可以继续

❌ 坑点2:忘记 await(最常见的错误)

Python
# ❌ 错误:创建了协程但没 await,任务永远不会执行
async def main():
    asyncio.create_task(fetch_data("A",1))
    # 函数结束了,但任务可能还没跑完!

# ✅ 正确:保存 task 引用并 await
async def main():
    task = asyncio.create_task(fetch_data("A",1))
    await task  # 等待任务完成

❌ 坑点3:用 async/await 做 CPU 密集型计算(没用!)

Python
# ❌ 错误:CPU 密集计算会阻塞事件循环
async def slow():
    for i in range(10**8):
        _ = i * i  # 没有 await,事件循环无法切换!

# ✅ 正确:用 run_in_executor 把计算放到线程池
async def slow_fixed():
    await asyncio.get_event_loop().run_in_executor(
        None,  # 使用默认线程池
        lambda: heavy_compute()  # 放到其他线程执行
    )
六、一句话总结 + 记忆口诀

🧠 记忆口诀

async def → 声明"我是协程,我可以暂停"
await → "这里要等,先去跑别的,好了叫我"
asyncio.run() → "开机,启动事件循环"
asyncio.gather() → "这些任务一起跑,全部完成再继续"
asyncio.create_task() → "把这个任务加入调度队列,马上返回"

✅ 什么时候用 async/await?

  • Web 爬虫(aiohttp / httpx)
  • Web 服务器(FastAPI 本身就是异步的!你正在用 🎯)
  • 批量 API 调用
  • WebSocket 长连接
  • 任何需要"同时等待多个 IO 操作"的场景

⚠️ 什么时候 不要 用?

  • CPU 密集型计算(用 multiprocessing)
  • 简单的脚本(同步代码更简单)
  • 调用了同步阻塞库且无法改为异步(考虑 run_in_executor)

🎉 现在你已经理解 async/await 了!

记住:await = "我先歇会儿,你先跑"