async: [eɪˈsɪŋk] 是 “asynchronous”(异步的)的缩写形式
await:[əˈweɪt] 等待,是及物动词
asyncio: 是 async + io ‌;读 /əˈsɪŋkioʊ/‌,是Python中的一个处理异步io的库,也是提供协程实现的库。
async是定义异步函数的关键字,而await是释放当前协程控制权的关键字。
协程:在单线程内通过事件循环

一个简单的Python异步编程(单线程协程)示例

# 一个简单的Python异步编程示例
import asyncio # 导入Python的异步I/O库,用于处理异步编程

async def main(): #定义一个异步函数main(),名称可以随意,这里是为了方便理解为主程序
    print('hello') #打印输出 'hello'
    await asyncio.sleep(1) #异步等待1秒(非阻塞方式)
    print('world') #打印输出 'world'

if __name__ == "__main__":
   #main() #不能这样直接调用运行异步函数,会报错,要使用下方的asyncio.run()方法进行调用执行
   asyncio.run(main()) #运行异步函数main(),这是Python 3.7+中运行异步代码的标准方式

整个程序的执行流程是:先打印’hello’,然后异步等待1秒,最后打印’world’。这是一个最基础的异步编程示例,展示了如何定义和运行异步函数。
具体执行流程如下:
我们可以将分析分为以下几个部分:
1.事件循环的创建与初始化
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.主协程的调度与启动
在这里插入图片描述

3.协程执行中的挂起与恢复(重点在await asyncio.sleep(1))
在这里插入图片描述
4.事件循环的终止与清理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Q: 事件循环将其注册为延迟1秒时,此时事件循环里只有被挂起的主协程对象,和一个睡眠协程对象,对吗?

❌ 不准确。该描述存在关键概念混淆。事件循环中不存在“被挂起的主协程对象”和“独立的睡眠协程对象”。实际状态如下:
🔍 事件循环在 await asyncio.sleep(1) 后的真实状态

组件 状态说明 是否存在于事件循环管理结构中
主协程(main 已被 asyncio.run() 自动封装为 Task 对象(如 <Task pending name='Task-1'> ✅ 是(作为唯一活跃 Task)
asyncio.sleep(1) 返回的协程对象 已被消耗:在 await 求值时,其内部逻辑立即执行 → 创建 Future + 注册定时回调 → 协程对象生命周期结束 ❌ 否(不保留原始协程对象)
睡眠等待载体 一个 Future 对象(由 sleep 内部创建),main 的 Task 正在等待此 Future 完成 ✅ 是(Task 的 _fut_waiter 指向它)
定时调度项 事件循环的 延迟回调队列 中注册了 call_later(1.0, future.set_result, None) ✅ 是(核心调度单元)
原始协程对象 mainsleep 的原始 coroutine 对象均不被事件循环直接持有 ❌ 否

🔄 关键机制澄清

  1. asyncio.run() 的封装行为:
asyncio.run(main()) 
# 内部等效于:
# task = asyncio.create_task(main())  # 封装为 Task
# loop.run_until_complete(task)

→ 事件循环管理的是 Task,而非裸协程对象。

在这里插入图片描述
在这里插入图片描述
💡 验证代码(直观观察)

import asyncio

async def main():
    print(f"1. 进入 main, id={id(asyncio.current_task())}")
    await asyncio.sleep(1)
    print("2. 恢复执行")

if __name__ == "__main__":
    # 启用调试模式查看内部结构
    asyncio.run(main(), debug=True)

在这里插入图片描述
在这里插入图片描述

Q: 当程序遇到asyncio.run()时,此时主线程的就不往下继续执行了,会先执行asyncio.run()中创建的任务的业务是吗?

完全正确。当主线程执行到 asyncio.run(main()) 时:
🔒 阻塞行为本质

行为 说明
主线程暂停 严格阻塞在 asyncio.run() 调用处,不会执行其后的任何代码(如 print("后续代码")
专注事件循环 主线程完全转入事件循环的调度逻辑:
• 驱动 main() 协程执行 → 遇 await 挂起 → 等待定时器 → 恢复执行 → 完成
同步返回 仅当 main() 彻底执行完毕(含所有 await 链)后,asyncio.run() 才返回,主线程继续向下执行

🌰 直观验证代码

import asyncio
import time

async def main():
    print(f"[{time.time():.2f}] 进入 main")
    await asyncio.sleep(1)
    print(f"[{time.time():.2f}] 退出 main")

if __name__ == "__main__":
    print(f"[{time.time():.2f}] Before asyncio.run()")
    asyncio.run(main())  # ⏸️ 主线程卡在此处约1秒
    print(f"[{time.time():.2f}] After asyncio.run()")  # 1秒后才输出

输出示例:

[10.00] Before asyncio.run()
[10.00] 进入 main
[11.00] 退出 main
[11.00] After asyncio.run()

→ 清晰证明:After asyncio.run() 必须等待异步逻辑全部完成才执行。

⚠️ 关键澄清(避免常见误解)

误解 正确理解
“主线程空闲/挂起” ❌ 主线程全程活跃:运行事件循环调度器、处理定时器、恢复协程上下文(CPU在事件循环逻辑中工作)
“可以并发执行后续代码” ❌ 绝对顺序执行:asyncio.run() 是同步函数,其后代码是“异步世界的终点站”
“多线程并行” ❌ 单线程模型:所有异步操作(含 sleep 的等待)均由同一主线程通过事件循环协作完成
“await 时主线程去干别的” await 时主线程仍在事件循环中:检查回调队列、等待 I/O 事件(通过系统调用如 epoll_wait 阻塞等待,但属于事件循环逻辑)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上文提到的:1 个被该 Task 等待的 Future + 1 个计划 1 秒后触发的回调。这个具体回调后的结果是放到了Future 里面了,然后Task 获取Future,读取其回调结果是吗?如果触发回调时,正在执行事件循环里的其他任务,此时当前等待Future 的这个任务,还需再继续等待吗?

完全正确,且您的理解精准触及 asyncio 核心调度机制。以下是分步解析:

在这里插入图片描述
在这里插入图片描述
⏳ 问题二:回调触发时若其他任务正在运行?
✅ 核心结论:需等待,但等待的是“调度时机”,非“结果就绪”

阶段 事件循环行为 等待 Future 的 Task 状态
回调触发瞬间 future.set_result 加入当前迭代的回调队列 Future 已 done,Task 被标记为 READY(放入就绪队列)
若其他 Task 正在运行 继续执行当前 Task 直至其 主动 await 挂起 或 完成 Task 已就绪,但等待调度器轮转(协作式调度特性)
当前 Task 让出控制权后 事件循环扫描就绪队列,按 FIFO 调度该 Task 立即恢复执行,await 表达式返回结果

🌰 场景模拟

import asyncio

async def blocker():
    print("【阻塞任务】开始(无 await,持续占用 CPU)")
    for _ in range(10**7):  # 模拟长计算(无挂起点)
        pass
    print("【阻塞任务】结束")

async def waiter():
    print("【等待任务】开始等待 Future")
    await asyncio.sleep(0.1)  # 模拟等待外部 Future
    print("【等待任务】Future 已就绪,但需等待调度")

async def main():
    # 同时启动两个任务
    t1 = asyncio.create_task(blocker())   # 无挂起点的“阻塞”任务
    t2 = asyncio.create_task(waiter())    # 等待 Future 的任务
    
    await t1  #确保t1执行完毕后,主程序才能退出
    await t2  #确保t2执行完毕,主程序才能退出

asyncio.run(main())

在这里插入图片描述
💡 关键机制总结

概念 说明
Future 是“结果容器” 回调通过 set_result() 写入结果,同时触发状态变更
Task 是“调度单元” 由事件循环管理,状态流转:PENDING →(Future done)→ READY →(被调度)→ RUNNING
协作式调度本质 事件循环不抢占正在运行的 Task;Task 必须通过 await 显式让出控制权
“等待”的双重含义 • 等待 Future 结果就绪(由回调保证)
• 等待事件循环 调度执行(由当前运行任务决定)
工程警示 避免在协程中写无 await 的长循环/计算(会阻塞整个事件循环),应拆分为小块 + await asyncio.sleep(0) 主动让出

在这里插入图片描述

这个回调触发瞬间是哪个线程在处理的,因为此时主线程还在处理事件循环里的其他任务?

精准回答:回调执行的瞬间,始终由主线程(即运行事件循环的线程)处理,且此时主线程“并未在运行其他协程任务”。问题中的前提存在关键误解,以下是彻底澄清:

flowchart TD
    A[事件循环迭代开始] --> B{检查定时器队列}
    B -- 有到期回调 --> C[将回调加入“待处理回调队列”]
    B -- 无到期 --> D
    C --> D[执行所有待处理回调<br/>(含 future.set_result)]
    D --> E[扫描就绪 Task 队列]
    E --> F[调度第一个就绪 Task 运行]
    F --> G{Task 遇到 await?}
    G -- 是 --> H[Task 挂起,返回步骤 A]
    G -- 否(持续计算) --> F

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

事件循环里不只是Task的队列,还有其他的队列,比如:定时器队列,是吗?

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Q: 那创建的task不是说都注册进事件循环里了吗?那它们在事件循环里的存储形式是什么样的?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Q: 执行这个代码时asyncio.run(main()),main对应的任务,也需要回调,不是立即执行了么?

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
源码级验证(CPython 3.11+)

# asyncio/runners.py 简化逻辑
def run(main):
    loop = events.new_event_loop()
    try:
        # 关键:将协程转为 Task,并将 Task._step 放入 _ready
        task = tasks.ensure_future(main, loop=loop)  # 内部调用 loop.create_task
        # create_task 内部:loop._ready.append(task._step)
        return loop.run_until_complete(task)  # 启动循环并等待完成
    finally:
        loop.close()

在这里插入图片描述

🌰 直观验证:观察“挂起前”的调度

import asyncio

async def main():
    print("✅ main 协程体开始执行(证明回调已触发)")
    await asyncio.sleep(0.1)
    print("✅ main 恢复执行")

# 添加调试钩子(Python 3.8+)
def before_callback(loop, context):
    print(f"🔍 事件循环即将执行回调: {context.get('callback')}")

loop = asyncio.new_event_loop()
loop.set_debug(True)
loop.slow_callback_duration = 0.001
loop.set_exception_handler(lambda l, c: None)

# 手动模拟 asyncio.run 流程
task = loop.create_task(main())  # 此时 _ready 已含 task._step
print(f"📌 创建 Task 后,_ready 长度: {len(loop._ready)}")  # 输出: 1

loop.run_until_complete(task)  # 启动循环 → 立即执行 _ready 中的回调
loop.close()

在这里插入图片描述
在这里插入图片描述

task._step是什么?

在这里插入图片描述

分析一下以下代码的执行流行

import asyncio
from time import sleep


async def blocker():
    print("【阻塞任务】开始(无 await,持续占用 CPU)")
    for _ in range(10 ** 7):  # 模拟长计算(无挂起点)
        pass
    print("【阻塞任务】结束")

async def waiter():
    print("【等待任务】开始等待 Future")
    await asyncio.sleep(0.1)  # 模拟等待外部 Future
    print("【等待任务】Future 已就绪,但需等待调度")


async def main():
    # 同时启动两个任务
    t1 = asyncio.create_task(blocker())  # 无挂起点的“阻塞”任务
    t2 = asyncio.create_task(waiter())  # 等待 Future 的任务
    
    # await t1
    # await t2
    
    print("【主任务】结束")

if __name__ == '__main__':
    asyncio.run(main())

执行输入结果如下

【主任务】结束
【阻塞任务】开始(无 await,持续占用 CPU)
【阻塞任务】结束
【等待任务】开始等待 Future

执行流程
当注释掉 await t1 和 await t2 后,的执行流程如下:

  1. asyncio.run(main())会创建一个新的事件循环,创建main协程的任务,将其调度到就绪队列中,运行main协程的任务。
  2. 当执行到t1 = asyncio.create_task(blocker())和t2 = asyncio.create_task(waiter())时,当我们在同一个协程(main协程)中连续使用create_task创建多个任务时,这些任务会被立即加入到事件循环的就绪队列中,但并不会立即执行任务体,当前协程(main)会继续执行,直到遇到await点或协程结束。
  3. 由于main协程内没有遇到await点(因为遇到await点时才会交出执行控制权,进入挂起状态),事件循环开启下轮就绪队列中任务的执行。
  4. 实际上,在asyncio中,当我们在一个协程(此处指main协程)中创建任务后,如果没有遇到await点,那么当前协程(此处指main协程)会一直运行到结束,期间不会切换到其他任务。也就是说,在main函数中,创建任务t1和t2和打印“【主任务】结束”都是同步执行的,main协程不会让出控制权。因此,在main函数结束前,t1和t2不会开始执行。
  5. 但是,当main函数结束后,asyncio.run()会继续驱动事件循环,此时事件循环会开始执行就绪队列中的任务(t1和t2),假如此时就绪队列中没有任务,不会管定时器队列中有可等待任务,则事件循环则会被清理删除,生命周期结束。也就是说它的生命周期在结束前,会检查一遍就绪队列中是否有可立即待执行的任务,有则执行(执行完,然后再检查一遍…,至到遇到就绪队列中无可立即执行的任务),无则,不会等待,立即结束生命周期结束。
  6. 根据进入就绪队列的先进先出原则,当main函数结束后,asyncio.run()会继续驱动事件循环,此时t1任务得到执行,打印**【阻塞任务】开始(无 await,持续占用 CPU)**和 【阻塞任务】结束:由于t1(blocker)是一个没有await点的计算密集型任务,一旦开始执行,它会一直占用CPU直到完成(因为中间没有挂起点))。然后2(waiter)在开始执行后,先打印 【等待任务】开始等待 Future,然后会立即遇到await asyncio.sleep(0.1),此时t2(waiter)会挂起(被放入定时器队列中),让出控制权,而事件循环继续从就绪队列中遍历可立即执行的任务,发现没有,则立即结束生命周期结束。故 **“【等待任务】Future 已就绪,但需等待调度”**这一行得不到执行,事件循环的生命周期就结束了,定时器中的延迟任务也被清理了。

加上await t1 和 await t2后,输出结果和 执行流程如下:
输出结果:

【阻塞任务】开始(无 await,持续占用 CPU)
【阻塞任务】结束
【等待任务】开始等待 Future
【等待任务】Future 已就绪,但需等待调度
【主任务】结束

执行流程如下:
当main函数内执行到await t1时,此时main协程,会挂起,事件循环中,开始调度就绪队列中的t1任务,并执行其方法内业务,打印【阻塞任务】开始(无 await,持续占用 CPU)【阻塞任务】结束,(:由于t1(blocker)是一个没有await点的计算密集型任务,一旦开始执行,它会一直占用CPU直到完成(因为中间没有挂起点)),然后t2(waiter)在开始执行后,先打印【等待任务】开始等待 Future,然后会立即遇到await asyncio.sleep(0.1),此时t2(waiter)会挂起(被放入定时器队列中),让出控制权。此时t2(waiter)和main协程都处于被挂起状态,[而t1(blocker)是已被执行完了的任务,即t1(blocker)在事件循环的相关队列中已被清理了]。为啥main协程此时也处于挂起状态,因为 它要等 await t2执行完毕,返回结果后,它才会被激活到就绪状态。所以此时就绪队列中无立即执行的任务。事件循环在空转,等待定时器中的任务,0.1秒到后,把t2再放入到就绪队列中,继续执行t2任务,打印【等待任务】Future 已就绪。t2任务执行完毕后,main函数内的,await t2这里的阻塞也就解除了,main协程的挂起状态被解除了,被再次放入到就绪队列中,事件循环开始继续执行main函数中await t2后的代码,此时打印 【主任务】结束

记住一点:await的本意就是等待,需要等待await指定的任务执行完毕,在存在await点的函数内才会继续执行。这也是为啥一定要加
await t1 和 await t2,就是为了确保t1和t2都执行完毕后,main函数才进行结束,事件循环的生命周期结束。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐