欢迎光临
我们一直在努力

协程不是线程:深入理解 Python async/await 运行机制

协程不是线程:深入理解 Python async/await 运行机制

“我以为 async 就是开了个新线程……” —— 几乎每一位第一次接触异步编程的同事

如果你也有过这样的困惑,这篇文章就是为你写的。


一、为什么要有异步编程?

在聊 async/await 之前,我们先回到一个最朴素的问题:同步代码哪里不够用了?

想象你是一家餐厅的服务员。同步模式下,你服务完1号桌(等他点菜、上菜、结账),才能去服务2号桌。如果1号桌的客人在纠结菜单,你就只能站在那里干等。

这就是同步 I/O 的本质:CPU 在等待网络、磁盘响应的过程中,什么都不做,白白浪费时间。

异步模式下,你把菜单留给1号桌,转身去招呼2号桌,等1号桌准备好了再回来。一个人,服务多张桌,没有多雇人(没有多开线程),效率却大幅提升。

这就是 Python 异步编程的核心动机。


二、async/await 的运行机制

2.1 从一个最简单的例子开始

import asyncio

async def say_hello():
print("Hello")
await asyncio.sleep(1) # 模拟 I/O 等待
print("World")

asyncio.run(say_hello())

运行结果:

Hello
(等待约1秒)
World

看起来和同步代码没什么区别?别急,我们加点料:

import asyncio
import time

async def task(name, delay):
print(f"[{name}] 开始")
await asyncio.sleep(delay)
print(f"[{name}] 完成,耗时 {delay}s")

async def main():
start = time.time()
# 并发执行三个任务
await asyncio.gather(
task("A", 2),
task("B", 1),
task("C", 3),
)
print(f"总耗时:{time.time() start:.2f}s")

asyncio.run(main())

输出:

[A] 开始
[B] 开始
[C] 开始
[B] 完成,耗时 1s
[A] 完成,耗时 2s
[C] 完成,耗时 3s
总耗时:3.01s

三个任务总共需要 2+1+3=6 秒,但实际只用了 3 秒。没有多线程,没有多进程,只有一个线程在跑。 这就是异步的魔法。

2.2 async 函数到底返回什么?

这是理解异步的第一个关键点。

async def greet():
return "你好"

result = greet()
print(result) # <coroutine object greet at 0x…>
print(type(result)) # <class 'coroutine'>

调用 async 函数,不会执行函数体,而是返回一个协程对象(coroutine object)。

协程对象就像一份"执行计划书",它描述了"要做什么",但还没有真正开始做。你需要把它交给 Event Loop 去调度执行。

2.3 await 做了什么?

await 是一个暂停点。当执行到 await 时,当前协程会:

  • 暂停自身执行,把控制权交还给 Event Loop
  • Event Loop 去执行其他就绪的协程
  • 等待的操作完成后,Event Loop 恢复这个协程,从暂停处继续执行
  • async def demo():
    print("步骤1:开始")
    await asyncio.sleep(0) # 暂停,让出控制权
    print("步骤3:恢复执行") # Event Loop 调度回来后继续

    async def other():
    print("步骤2:趁机执行")

    async def main():
    await asyncio.gather(demo(), other())

    asyncio.run(main())

    输出:

    步骤1:开始
    步骤2:趁机执行
    步骤3:恢复执行

    这就是协程的"协作式调度"——主动让出,而非被动抢占。


    三、协程对象、Task、Event Loop 三者的关系

    这是很多人最容易混淆的地方,我用一个类比把它讲清楚。

    类比:剧本、演员、导演

    概念类比职责
    协程对象(coroutine) 剧本 描述"要做什么",是惰性的,不会自己执行
    Task 演员 包裹剧本,负责实际执行,有状态(pending/running/done)
    Event Loop 导演 统筹调度所有演员,决定谁上场、谁等待

    3.1 协程对象:惰性的执行计划

    async def fetch_data():
    await asyncio.sleep(1)
    return {"data": 42}

    # 只是创建了协程对象,什么都没发生
    coro = fetch_data()
    print(coro) # <coroutine object fetch_data at 0x…>

    协程对象本身不会运行,它需要被"驱动"。

    3.2 Task:被调度的执行单元

    Task 是对协程的封装,创建 Task 后,Event Loop 会在合适的时机调度它执行。

    async def main():
    # 方式1:asyncio.create_task(推荐)
    task1 = asyncio.create_task(fetch_data())

    # 方式2:asyncio.ensure_future(较旧的写法)
    task2 = asyncio.ensure_future(fetch_data())

    # Task 创建后立即被调度,不需要 await 就已经"启动"了
    print(task1) # <Task pending name='Task-1' coro=<fetch_data()>>

    # await 等待 Task 完成并获取结果
    result = await task1
    print(result) # {'data': 42}

    关键区别:

    async def main():
    # 顺序执行:先等 A 完成,再执行 B
    await asyncio.sleep(2) # 等2秒
    await asyncio.sleep(2) # 再等2秒
    # 总耗时:4秒

    # 并发执行:A 和 B 同时跑
    task_a = asyncio.create_task(asyncio.sleep(2))
    task_b = asyncio.create_task(asyncio.sleep(2))
    await task_a
    await task_b
    # 总耗时:2秒

    3.3 Event Loop:一切的核心调度器

    Event Loop 是异步程序的心脏,它做的事情本质上是一个无限循环:

    while True:
    1. 检查哪些 Task 已经就绪(I/O 完成、定时器到期等)
    2. 执行就绪的 Task,直到它遇到下一个 await
    3. 处理 I/O 事件(通过 select/epoll/kqueue)
    4. 重复

    用代码感受一下 Event Loop 的存在:

    import asyncio

    async def main():
    loop = asyncio.get_event_loop()
    print(f"当前 Event Loop:{loop}")
    print(f"是否在运行:{loop.is_running()}")

    asyncio.run(main())

    asyncio.run() 做了三件事:创建 Event Loop → 运行传入的协程直到完成 → 关闭 Event Loop。

    3.4 三者协作的完整流程

    你的代码

    ├─ 调用 async 函数 ──→ 生成 协程对象(coroutine)

    ├─ asyncio.create_task() ──→ 包装成 Task,注册到 Event Loop

    └─ asyncio.run() / await


    Event Loop

    ├─ 调度 Task A(运行到 await,暂停)
    ├─ 调度 Task B(运行到 await,暂停)
    ├─ I/O 完成,Task A 就绪,恢复执行
    └─ Task A 完成,返回结果


    四、如何向只写同步代码的同事解释"协程不是线程"

    这是我在团队里真实遇到过的场景。下面是我用过的最有效的解释方式。

    4.1 先破除误解

    同事的直觉:“async 函数会在后台开一个新线程跑。”

    验证一下:

    import asyncio
    import threading

    async def check_thread():
    print(f"协程运行在线程:{threading.current_thread().name}")
    await asyncio.sleep(0.1)
    print(f"恢复后还是线程:{threading.current_thread().name}")

    async def main():
    print(f"主线程:{threading.current_thread().name}")
    await asyncio.gather(
    check_thread(),
    check_thread(),
    check_thread(),
    )

    asyncio.run(main())

    输出:

    主线程:MainThread
    协程运行在线程:MainThread
    协程运行在线程:MainThread
    协程运行在线程:MainThread
    恢复后还是线程:MainThread
    恢复后还是线程:MainThread
    恢复后还是线程:MainThread

    三个协程,全部在同一个线程里运行。 这是铁证。

    4.2 用"单线程的并发"来描述

    我会这样跟同事说:

    “线程是操作系统级别的并发,由 OS 调度,可以真正并行(多核)。协程是用户态的并发,由 Event Loop 调度,同一时刻只有一个协程在执行,但它们可以在等待 I/O 时互相切换,从而实现并发效果。”

    4.3 展示最关键的差异:阻塞行为

    import asyncio
    import time

    # ❌ 错误示范:在协程里用同步阻塞调用
    async def bad_task(name):
    print(f"[{name}] 开始")
    time.sleep(2) # 这会阻塞整个 Event Loop!
    print(f"[{name}] 完成")

    # ✅ 正确做法:用异步版本
    async def good_task(name):
    print(f"[{name}] 开始")
    await asyncio.sleep(2) # 只暂停当前协程,不阻塞其他
    print(f"[{name}] 完成")

    async def main_bad():
    start = time.time()
    await asyncio.gather(bad_task("A"), bad_task("B"))
    print(f"bad 总耗时:{time.time() start:.2f}s") # 4秒

    async def main_good():
    start = time.time()
    await asyncio.gather(good_task("A"), good_task("B"))
    print(f"good 总耗时:{time.time() start:.2f}s") # 2秒

    asyncio.run(main_bad())
    asyncio.run(main_good())

    这个对比是最直观的教学工具。time.sleep() 会冻结整个 Event Loop,因为它阻塞了线程;asyncio.sleep() 只是告诉 Event Loop “我先歇一会儿,你去忙别的”。

    4.4 一张对比表,贴在工位上

    维度线程(Thread)协程(Coroutine)
    调度者 操作系统 Event Loop(用户态)
    切换时机 任意时刻(抢占式) 遇到 await(协作式)
    内存开销 约 1-8 MB/个 约 几 KB/个
    并发数量 数百到数千 轻松数万
    适合场景 CPU 密集型 I/O 密集型
    共享状态风险 需要锁(race condition) 单线程,相对安全
    调试难度 较高(竞态条件) 中等(回调地狱已消失)

    4.5 一个实战案例:并发抓取网页

    import asyncio
    import aiohttp
    import time

    # 模拟 URL 列表
    URLS = [f"https://httpbin.org/delay/1" for _ in range(5)]

    async def fetch(session, url, idx):
    async with session.get(url) as response:
    data = await response.json()
    print(f"请求 {idx} 完成,状态码:{response.status}")
    return data

    async def main():
    start = time.time()
    async with aiohttp.ClientSession() as session:
    tasks = [fetch(session, url, i) for i, url in enumerate(URLS)]
    results = await asyncio.gather(*tasks)
    print(f"5个请求总耗时:{time.time() start:.2f}s")
    # 每个请求需要1秒,但并发执行,总耗时约1秒

    asyncio.run(main())

    如果用同步 requests 逐个请求,需要约 5 秒。异步并发只需约 1 秒。同一个线程,5倍的效率提升。


    五、常见陷阱与最佳实践

    陷阱1:忘记 await

    async def main():
    # ❌ 忘记 await,协程对象被创建但从未执行
    fetch_data() # RuntimeWarning: coroutine 'fetch_data' was never awaited

    # ✅ 正确
    await fetch_data()

    陷阱2:在协程中调用阻塞函数

    import asyncio
    from concurrent.futures import ThreadPoolExecutor

    # 对于无法避免的同步阻塞操作(如某些第三方库)
    # 用 run_in_executor 放到线程池里跑
    async def safe_blocking_call():
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, time.sleep, 2)
    return result

    陷阱3:混淆 gather 和顺序 await

    async def main():
    # 顺序执行,总耗时 = 各任务之和
    await task_a()
    await task_b()

    # 并发执行,总耗时 = 最长任务的耗时
    await asyncio.gather(task_a(), task_b())


    六、总结

    async/await 的本质是单线程内的协作式并发,三个核心概念的关系一句话概括:

    协程对象是执行计划,Task 是被调度的执行单元,Event Loop 是统筹一切的调度器。

    协程不是线程,它不会神奇地并行执行 CPU 计算,但在 I/O 密集型场景(网络请求、文件读写、数据库查询)下,它能用极低的资源开销实现极高的并发吞吐。

    理解了这一点,你就掌握了 Python 异步编程的灵魂。


    互动讨论

    • 你在项目中有没有遇到过"把同步库用在异步代码里导致性能反而下降"的情况?是怎么排查和解决的?
    • 你认为 Python 的 GIL 和异步编程之间是什么关系?欢迎在评论区聊聊你的理解。

    参考资料

    • Python 官方 asyncio 文档
    • PEP 492 – Coroutines with async and await syntax
    • 书籍推荐:《流畅的Python(第2版)》第19-21章,David Beazley 的《Python Cookbook》
    • 库推荐:aiohttp(异步 HTTP)、aiomysql(异步 MySQL)、aiofiles(异步文件 I/O)
    赞(0)
    未经允许不得转载:171主机测评 » 协程不是线程:深入理解 Python async/await 运行机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址