30. 协程
1.协程的概念
1.1 定义
进程是操作系统内部运行的程序,线程是进程内部运行的程序,协程是线程内部运行的程序
协程是单线程下的并发,又成微线程,英文名coroutine
计算机只有进程和线程,协程是开发人员创造的名称
协程是一种用户态的上下文切换技术,通过一个线程实现代码块切换运行
1.2 协程的优点
协程切换的开销更小
GIL锁导致同一时刻只能运行一个线程,一个线程内不会限制协程数,单线程就可以实现并发的效果,最大程度利用CPU
1.3 协程的缺点
协程的本质是单线程下的多任务处理,无法利用多核优势
如果协程发生阻塞,在没有使用异步I/O的情况下,那么整个线程将阻塞,所在线程的其它协程任务都不能运行
1.4 python异步编程(协程)引出
问题场景:I/O 密集型任务的特点是:程序在执行过程中,需要频繁等待输入/输出操作完成。
例如:
等待网络请求返回数据
等待磁盘读写完成
等待数据库查询结果
同步编程在处理I/O密集型任务的等待期间,程序会阻塞,CPU 处于空闲,从而降低了效率。
代码模拟这个问题:
import time
def f1():
time.sleep(6) # 模拟一个耗时6秒的I/O操作
return 111
def f2():
time.sleep(3) # 模拟一个耗时3秒的I/O操作
return 222
def work():
res1 = f1()
print('f1函数的运行结果:', res1)
res2 = f2()
print('f2函数的运行结果:', res2)
if __name__ == '__main__':
start = time.time()
work()
print('总耗时:', time.time() - start)

f1() 运行时,程序进入 sleep(6),必须等待6秒才能继续。f2() 只有在f1() 完成后才能开始,又等待 3 秒。整体耗时大约9秒。
在同步编程中,一旦遇到 I/O 阻塞,整个程序会停下来等待。这导致:CPU 资源被浪费,整体运行效率低,单位时间内完成的任务量少。
理想情况应该是:
当 f1 处于等待时,CPU 可以切换去运行任务(比如 f2),而不是原地等待。这样程序可以同时运行多个任务,从而提升效率。这就是异步编程要解决的核心问题。
2 异步编程(协程)相关概念
2.1 async
[1] 协程函数
在 Python 中,async 关键字主要用于定义异步函数(协程函数),这种函数可以运行非阻塞操作(通过 await 实现),并允许在等待某些任务完成时,程序继续运行其他任务。
加上async关键字的函数就是协程函数
async def f1():
...
[2] asyncio模块
Python 3.4:asyncio 被正式引入标准库,是一个实现异步编程的模块。
Python 3.5:引入了 async 和 await 关键字,这使得协程的定义和使用更加直观和简洁。这些关键字取代了早期版本中的 @asyncio.coroutine 装饰器和 yield from 语法。
[3] 协程对象
协程函数调用后的返回值就是协程对象
async def f1():
print(111)
res = f1()
print(res)

import asyncio
async def f1():
print(666)
def work():
obj = f1() # <class 'coroutine'>
print(type(obj))
if __name__ == '__main__':
work()

关于以上代码:
协程函数的返回值不是函数运行的结果,而是一个协程对象。
协程对象是一个未开始运行的任务。要运行这个任务,必须通过事件循环来调度
[4] 核心概念
| 方面 | 异步函数 (Async Function) | 协程 (Coroutine) |
|---|---|---|
| 定义 | 用 async def 定义的函数,是「协程(协程对象)的创建器」 |
异步函数调用后返回的可等待对象,是「可被await调度运行的异步任务单元」 |
| 类型 | function 类型 |
coroutine 类型 |
| 调用 | 调用时返回协程(协程对象) | 本身就是可等待对象 |
| 关系 | 异步函数是创建协程(协程对象)的工具,调用后才生成协程对象 | 异步函数的实例 |
| 复用 | 可多次调用 | 通常只能被 await 一次 |
| await | await 等待的是协程(协程对象)而非异步函数 | |
[5] 事件循环
(1) 概念事件循环可以类比while循环来理解,在循环周期内运行一些任务,特定的条件下结束循环。
import asyncio
loop = asyncio.get_event_loop() # 完整版,另外有简化版本
import asyncio
async def work():
print(666)
loop = asyncio.run(work()) # 简化版

import asyncio
async def f1():
print('hello')
def work():
obj = f1()
# 创建事件循环,并将obj协程对象注册到事件循环中,由事件循环调度运行
asyncio.run(obj)
if __name__ == '__main__':
work()

Python 中的异步编程之所以需要使用协程而不是普通函数,主要是因为协程能够支持暂停和恢复执行,而普通函数则不能。
[6] 协程函数的两种调用方法详解,以解释器3.10版本为例
(1) 方法一(底层版):import asyncio
async def work():
print(666)
return 'work函数的返回值'
def create_coroutine():
obj = work() # 1.调用协程函数,生成协程对象
circle = asyncio.get_event_loop() # 2.建立一个事件循环
circle.run_until_complete(obj) # 3.将协程对象当作任务提交到事件循环的任务列表中,协程运行完成事件循环停止
if __name__ == '__main__':
create_coroutine()

在 Python 3.10 中,这个警告是因为在没有活动事件循环的主线程中调用 asyncio.get_event_loop() 时,get_event_loop() 会自动创建一个新的事件循环,这种方式被认为是不推荐的。
为了避免这个警告,可以使用 asyncio.run() 来运行协程,这是从 Python 3.7 开始推荐的做法。asyncio.run() 会自动创建和关闭事件循环,简化了异步代码的编写。
import asyncio
async def work():
print(666)
return 'work函数的返回值'
def create_coroutine():
obj = work() # 1.调用协程函数,生成协程对象
asyncio.run(obj) # 2.调用run函数启动协程对象
if __name__ == '__main__':
create_coroutine()

方式二的本质和方式一是一样的,内部先建立事件循环,然后运行run_until_complete将异步协程对象提交到事件循环中
需要注意的是,run函数在解释器3.7才加入
2.2 await关键字
[1] 理论
概念:await是用于异步编程的关键字,实现非阻塞的等待操作。只能在 async 函数内部使用,不能在非异步函数中使用。
作用:用于暂停当前协程(协程对象)的运行,直到 await 后面的表达式完成(等待可等待对象完成并返回结果),期间事件循环可调度其他协程运行,从而实现异步并发。
await的核心规则是:只有当被 await 的 “可等待对象”(如 future/task/ 协程)处于 “未完成状态” 时,当前协程才会挂起;
异步协程 “协作式调度” 的核心:协程只会在明确的挂起点(await 未完成的可等待对象)交出控制权,而非一遇到 await 就挂起。
await必须和可等待对象搭配使用。
可等待对象:
| 协程(Coroutine) | async def 定义的函数调用后返回的对象(最常用) |
| 任务(Task) | 封装协程的可并发对象(由 asyncio.create_task() 创建,是 Future 子类) |
| 未来对象(Future) | 低层级异步结果容器(一般无需手动创建,框架 / 库内部使用) |
事件循环(Event Loop):
异步编程的「调度中心」,负责管理所有协程 / 任务的执行:
当协程(协程对象)运行到 await 时,会将控制权交还给事件循环;
事件循环会调度其他就绪的任务执行;
当 await 等待的对象完成后,事件循环会唤醒该协程,继续运行后续代码。
[2] await用法
(1) await 协程(协程对象)
import asyncio
import time
# 定义异步函数(协程函数)
async def work(delay, what):
# 等待 asyncio.sleep(非阻塞睡眠,区别于 time.sleep)
await asyncio.sleep(delay)
print(what)
# 主协程
async def main():
# 串行执行两个协程(await 逐个等待)
await work(1, "avril")
await work(2, "lavigne")
if __name__ == "__main__":
start = time.time()
print(f"主函数运行开始")
asyncio.run(main())
print(f"主函数运行结束") # 运行事件循环
end = time.time()
print(f'主函数运行时长{end - start}')
asyncio.run(main ()) 会做 3 件事:创建新的事件循环、运行 main() 协程直到完成、关闭事件循环。
调用 main() 生成「main 协程对象」,事件循环开始驱动该协程运行。
运行第一个 await work (1, "avril"):
调用 work(1, "avril") → 生成work 协程对象(delay=1, what=’avril’)
遇到 await → 挂起 main 协程,事件循环转而运行work 协程对象(delay=1, what=’avril’)
[注意这条代码运行在main协程下,await挂起的是当前正在运行的协程,而非被await的对象]
运行 work(1, "avril") 的函数体:
await asyncio.sleep(1) # 遇到await → 挂起work协程,控制权交还事件循环
# 事件循环等待1秒,期间无其他任务,仅等待
#1秒后,asyncio.sleep(1)完成 → 恢复work协程
# 控制台输出「avril」
work(1, "avril") 协程运行完毕 → 控制权交还事件循环;
恢复 main 协程,继续向下运行。
为何await work(1, "avril")会调度该函数体而非其他代码:
被 await 的协程对象属于 “未执行的可等待对象”,事件循环需执行其函数体以推进至完成态,且无其他可调度对象竞争;
await的语义是 “交出控制权至事件循环,要求事件循环推进被 await 对象至完成态”:
对于协程对象,“推进至完成态” 的唯一方式是执行其函数体;
当main协程因await work(1, "avril")挂起并移交控制权时,事件循环的调度队列(Scheduling Queue) 中仅存在这一个协程对象(第二个work(2, "lavigne")尚未生成,无其他可调度对象),因此事件循环只能选择调度该协程对象的函数体。
事件循环无其他已提交的可调度对象(Task/Coroutine/Future),因此无 “其他代码” 可调度,只能运行work(1, "avril")的函数体。
为何await asyncio.sleep(1)会原地等待而非调度其他代码:
asyncio.sleep 返回的 Future 对象依赖 “时间触发完成”,且事件循环调度队列中无其他可调度对象,进入空闲等待状态。
asyncio.sleep(1)返回的是Future 对象(具体为 TimerHandle 驱动的 Future),这类 Future 的完成条件是 “事件循环的时钟达到延迟时间”,而非 “执行代码逻辑”。
其核心特性:
Future 对象的 “完成态” 由事件循环的I/O 多路复用器(如 select/poll/epoll) + 时钟调度器(TimerHandle) 触发,无需执行函数体;
当work(1, "avril")协程执行至await asyncio.sleep(1)时,该 Future 被提交至事件循环的延迟任务队列(Delayed Task Queue),事件循环会将控制权移交至 I/O 多路复用器,等待 Future 完成。
“原地等待” 的本质是事件循环无其他可调度对象,而非asyncio.sleep本身阻塞:
此时第二个work(2, "lavigne")协程对象尚未生成,调度队列中无任何 Task/Coroutine/Future(除了该 sleep 对应的 Future);
事件循环的调度逻辑是 “优先调度就绪队列中的对象,无就绪对象则等待延迟队列 / IO 事件”,因此只能进入 “空闲等待” 状态,直至 sleep 的 Future 因时间到达被标记为 “完成态”。
对比:若有其他可调度对象则不会 “原地等”
若代码改为并行模式(提前生成多个可调度对象):
async def main():
task1 = asyncio.create_task(work(1, "avril"))
task2 = asyncio.create_task(work(2, "lavigne"))
await asyncio.gather(task1, task2)
此时work(1, "avril")执行至await asyncio.sleep(1)挂起时,事件循环调度队列中存在task2(work(2, "lavigne")),会立即调度task2的函数体执行,而非 “原地等待”—— 这验证了 “原地等” 的核心原因是 “无其他可调度对象”,而非asyncio.sleep的特性。
|
场景 |
调度行为原因 |
核心依赖因素 |
|
await work(1, "avril") |
协程对象需执行函数体才能完成,且无其他可调度对象 |
协程对象的未执行特性 + 调度队列空 |
|
await asyncio.sleep(1) |
Future 依赖时间触发完成,且调度队列无其他可调度对象,事件循环进入空闲等待 |
Future 的时间完成机制 + 调度队列空 |
运行第二个 await work (2, "lavigne"):
调用 work(2, "lavigne") → 生成「work 协程对象(delay=2)」;
遇到 await → 再次挂起 main 协程,事件循环执行「work 协程对象(delay=2)」;
运行work(2, "lavigne") 的函数体:
await asyncio.sleep(2) 挂起work协程,事件循环等待2秒
print(what) 2秒后,恢复work协程 → 控制台输出「lavigne
work(2, "lavigne") 协程执行完毕 → 控制权交还事件循环;
恢复 main 协程,main 协程的所有代码执行完毕。
事件循环收尾:
main 协程执行完毕 → asyncio.run() 关闭事件循环 → 回到主程序的同步代码流程。
asyncio.sleep(delay):非阻塞,挂起当前协程,事件循环可调度其他任务(本例无其他任务,所以看似 “等待”),返回一个 delay 秒后完成的 Future 对象。
time.sleep(delay):阻塞线程,事件循环会被卡住,即使有其他协程也无法执行。
await 的本质await 不是 “等待”,而是 “交出控制权”:当前协程暂停,事件循环去处理其他可等待对象,直到目标对象完成,再恢复当前协程。
(2) await Task 实现并发
单独 await 协程是串行的,要实现并发,需先将协程封装为 Task
Task:asyncio.create_task() 将协程包装为 “可调度的任务”,立即加入事件循环的待执行队列。
import asyncio
import time
# 定义异步函数(协程函数)
async def work(delay, what):
await asyncio.sleep(delay) # 等待 asyncio.sleep(非阻塞睡眠,区别于 time.sleep)
print(what)
# 主协程
async def main():
# 创建 Task(立即调度,非阻塞)
task1 = asyncio.create_task(work(1, "avril"))
task2 = asyncio.create_task(work(2, "lavigne"))
# 等待两个 Task 完成(并发执行)
await task1
await task2
if __name__ == "__main__":
start = time.time()
print(f"主函数运行开始")
asyncio.run(main())
print(f"主函数运行结束") # 运行事件循环
end = time.time()
print(f'主函数运行时长{end - start}')
create_task 会立即将协程加入事件循环调度,无需等待;
两个 Task 并发执行,总耗时 2 秒(最长的那个任务的耗时)。
asyncio.run(main())
创建新的事件循环
将 main() 作为「根协程」加入事件循环
运行事件循环直到 main 完成
关闭事件循环
[进入main协程运行]
task1 = asyncio.create_task(work(1, "avril")) :
调用 work(1, "avril") → 返回协程对象()
create_task 将协程包装为 Task,立即加入事件循环的「待执行队列」
此步骤非阻塞,立即返回 task1 对象
task2 = asyncio.create_task(work(2, "lavigne")) :
逻辑同 task1:创建协程对象 → 包装为 Task → 加入待执行队列
此时事件循环待执行队列:[task1, task2]
await task1
挂起 main 协程,将控制权交回事件循环
事件循环开始调度待执行队列中的任务(先处理 task1)
事件循环调度 task1,运行work (1, "avril") 协程:
async def work(delay, what):
await asyncio.sleep(delay)
运行work(1, "avril") 的 await asyncio.sleep(1)
创建一个「1秒后完成的 Future 对象」;
await 该 Future → 挂起 work(1, "avril") 协程,控制权交回事件循环;
事件循环此时无其他可执行任务,调度下一个待运行的 task2。
(暂时未执行)print(what)
事件循环调度 task2,运行work (2, "lavigne") 协程:
运行work(2, "lavigne") 的 await asyncio.sleep(2)
创建一个「2秒后完成的 Future 对象」;
await 该 Future → 挂起 work(2, "lavigne") 协程,控制权交回事件循环;
此时所有 Task 都被挂起(task1 等1秒,task2 等2秒),事件循环进入「等待状态」(无CPU占用)。
(暂时未执行)print(what)
Future 完成,协程恢复运行:
主程序启动1秒后,asyncio.sleep(1) 的 Future 完成:
事件循环检测到 Future 完成 → 唤醒 work(1, "avril") 协程
运行print(what) → 控制台输出「avril」
work(1, "avril") 协程执行完毕 → task1 任务完成。
事件循环检测到 task1 完成 → 唤醒被挂起的 main 协程:
main 协程从 await task1 处恢复,运行下一步 await task2
await task2 → 再次挂起 main 协程,等待 task2 完成(此时 task2 已等待 1 秒,还剩 1 秒
主程序启动2 秒后:asyncio.sleep(2) 的 Future 完成
事件循环检测到 Future 完成 → 唤醒 work(2, "lavigne") 协程;
运行print(what) → 控制台输出「lavigne」;
work(2, "lavigne") 协程运行完毕 → task2 任务完成。
main 协程完成,事件循环关闭:
事件循环检测到 task2 完成 → 唤醒 main 协程;
main 协程从 await task2 处恢复,无更多代码 → main 协程运行完毕。
asyncio.run(main()) 完成 → 关闭事件循环,回到主函数。
2.3Task
[1]概念
前面部分代码只创建了一个任务,即事件循环的列表中只有一个任务对象;如需在程序中创建多个任务对象,需要使用Task。
Task用于并发调度协程,通过asyncio.create(协程对象)的方式创建Task对象,可以让协程加入事件循环中等待被调度执行。
注意事项:
asyncio.create_task( )函数在python3.7中被加入,在之前的版本可以改用底层级的asyncio.ensure_future( )函数。
[2]创建多任务方式一
逐个创建任务
import asyncio
# 定义协程功能函数
async def work():
print('协程运行开始')
await asyncio.sleep(1) # 模拟异步操作,比如网络请求
print('协程运行结束')
return 666
# 定义产生协程函数
async def create_coroutine():
print('产生协程的函数运行开始')
task1 = asyncio.create_task(work()) # 将work()得到的协程对象封装到Task对象中,并立即添加到事件循环的任务列表中,等待事件循环
task2 = asyncio.create_task(work()) # 将work()得到的协程对象封装到Task对象中,并立即添加到事件循环的任务列表中,等待事件循环
response1 = await task1 # 引用了await之后,task1遇到sleep不会阻塞整个线程
response2 = await task2
print(response1, response2)
print('产生协程的函数运行结束')
if __name__ == '__main__':
asyncio.run(create_coroutine())

[3]创建多任务方式二
使用列表生成式生成任务
import asyncio
# 定义协程功能函数
async def work(num):
print('协程运行开始')
await asyncio.sleep(1) # 模拟异步操作,比如网络请求
print('协程运行结束')
return num * num
# 定义产生协程函数
async def create_coroutine():
print('产生协程的函数运行开始')
# 将work()得到的协程对象封装到Task对象中,并立即添加到事件循环的任务列表中,等待事件循环
task_list = [asyncio.create_task(work(i)) for i in range(1, 4)]
# 引用了await之后,task1遇到sleep不会阻塞整个线程
# 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中
# wait里面要放可迭代对象
done, pending = await asyncio.wait(task_list, timeout=None)
print(done)
print(pending)
print('产生协程的函数运行结束')
if __name__ == '__main__':
asyncio.run(create_coroutine())

2.4 gather方法
[1] 概念
asyncio.gather 是 Python 异步编程中批量管理、并发执行多个可等待对象的核心 API,用于替代「逐个 await Task」的写法,能更简洁地实现多任务并发,并统一收集结果。
作用:
并发执行多个协程 / Task/Future(可等待对象)
等待所有传入的可等待对象完成(或指定 return_exceptions=True 时忽略异常)
按传入顺序返回所有任务的结果(即使任务执行快慢不同)
[2] 代码示例
用法1:并发运行多个协程,收集结果
gather 自动将传入的协程封装为 Task 并加入事件循环,无需手动 create_task
import asyncio
import time
# 定义异步函数(协程函数)
async def work(name, delay):
print(f'任务{name}运行开始,延迟{delay}秒')
await asyncio.sleep(delay) # 等待 asyncio.sleep(非阻塞睡眠,区别于 time.sleep)
print(f'任务{name}运行结束')
return f'任务{name}的返回值'
# 主协程
async def main():
# 用gather并发运行多个协程
# 传入多个协程对象,gather会自动封装为Task且并发运行
res = await asyncio.gather(
work('a', 3),
work('b', 2),
work('c', 1)
)
# 打印结果(顺序与传入顺序一致)
print(res)
if __name__ == "__main__":
start = time.time()
print(f"主函数运行开始")
asyncio.run(main())
print(f"主函数运行结束") # 运行事件循环
end = time.time()
print(f'主函数运行时长{end - start}')

用法2:传入已创建的gather对象
若需要提前创建 Task(比如要给任务命名、手动控制调度),也可将 Task 对象传入 gather
import asyncio
import time
# 定义异步函数(协程函数)
async def work(name, delay):
print(f'任务{name}运行开始,延迟{delay}秒')
await asyncio.sleep(delay) # 等待 asyncio.sleep(非阻塞睡眠,区别于 time.sleep)
print(f'任务{name}运行结束')
return f'任务{name}的返回值'
# 主协程
async def main():
# 手动创建Task
task1 = asyncio.create_task(work('a', 2))
task2 = asyncio.create_task(work('b', 1))
# 传入Task对象(效果与方法1传入协程对象一致)
res = await asyncio.gather(task1, task2)
print(res)
if __name__ == "__main__":
start = time.time()
print(f"主函数运行开始")
asyncio.run(main())
print(f"主函数运行结束") # 运行事件循环
end = time.time()
print(f'主函数运行时长{end - start}')

用法3:高级特性
动态批量传入任务(解包列表);若任务数量不确定(比如从列表动态生成),可用 * 解包列表传入。
在2.3Task使用列表生成式生成任务的基础上使用async.gather()获取返回值
import asyncio
# 定义协程功能函数
async def work(num):
print('协程运行开始')
await asyncio.sleep(1) # 模拟异步操作,比如网络请求
print('协程运行结束')
return num * 10
# 定义产生协程函数
async def create_coroutine():
print('产生协程的函数运行开始')
# 将work()得到的协程对象封装到Task对象中,并立即添加到事件循环的任务列表中,等待事件循环
task_list = [asyncio.create_task(work(i)) for i in range(1, 4)]
# 引用了await之后,task1遇到sleep不会阻塞整个线程
# 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中
# wait里面要放可迭代对象
done, pending = await asyncio.wait(task_list, timeout=None)
response = await asyncio.gather(*task_list)
print(response)
print('产生协程的函数运行结束')
if __name__ == '__main__':
asyncio.run(create_coroutine())

需要注意的是,以上代码即使不运行 done, pending = await asyncio.wait(task_list, timeout=None),也能获取到返回值。
2.5 future
[1] 概念
(1) 定义
Future 是 Python asyncio 异步框架中底层的核心组件,是「异步操作结果的占位符」—— 它代表一个 “尚未完成、未来才会产生结果(或异常)的异步操作”。
(2) 本质:异步结果的 “容器 / 占位符”
创建 Future 对象时,异步操作尚未完成,容器内无结果;
当异步操作完成(成功 / 失败),会将「结果」或「异常」写入这个容器;
协程可通过 await 等待 Future,直到容器内有结果(或异常)。
(3) 状态
| 状态 | 说明 |
|---|---|
Pending |
初始状态,异步操作未完成(默认状态) |
Done |
终态,异步操作已完成(包含两种子状态):
Finished:成功,有返回结果;
Exception:失败,有异常;
Cancelled:被取消(特殊的 Done 状态) |
[2] 用法
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
def thread_task(futrue):
time.sleep(5)
futrue.set_result(100)
async def sub_task():
print('sub task 开始')
# 创建 future 对象
event_loop = asyncio.get_running_loop()
future = event_loop.create_future()
# 创建线程池对象
executor = ThreadPoolExecutor()
# 在其他线程执行任务
event_loop.run_in_executor(executor, thread_task, future)
# 挂起当前任务,事件循环调度其他任务执行
result = await future
print('sub task 结束')
return result
async def task1():
print('task1 开始')
result = await sub_task()
print('task1 结束')
return result
async def task2():
print('task2 开始')
await asyncio.sleep(1)
print('task2 结束')
return 200
async def main():
result = await asyncio.gather(task1(), task2())
print(result)
if __name__ == '__main__':
asyncio.run(main())
asyncio.gather:并发调度多个协程,等待所有协程完成后返回结果列表(顺序与传入一致);
task1()/task2():调用协程函数仅创建「协程对象」,不会立即执行,需由事件循环调度。
事件循环将task1、task2包装为「Task 对象」,加入任务队列,开始调度执行。
事件循环优先调度第一个任务task1:
执行print('task1 开始') → 控制台输出:task1 开始。
await sub_task() 触发sub_task协程执行,事件循环进入sub_task。
执行print('sub task 开始') → 控制台输出:sub task 开始。
event_loop = asyncio.get_running_loop() → 获取当前运行的事件循环(asyncio.run创建的那个)。
future = event_loop.create_future() → 创建Future对象(异步结果容器),初始状态为pending(未完成)。
executor = ThreadPoolExecutor() → 创建线程池(默认线程数 = CPU 核心数 ×5)。
event_loop.run_in_executor(executor, thread_task, future)
核心逻辑:将thread_task(future)提交到线程池执行(非阻塞事件循环);
线程池创建子线程,执行thread_task:
子线程执行time.sleep(5) → 线程阻塞 5 秒(不影响事件循环);
result = await future
await future 检查future状态(当前是pending),因此sub_task被挂起,交出事件循环控制权;
事件循环转而调度任务队列中的下一个任务:task2。
调度执行task2:
执行print('task2 开始') → 控制台输出:task2 开始。
await asyncio.sleep(1)
asyncio.sleep是异步 sleep(非阻塞事件循环)
事件循环创建「1 秒定时器」,将task2挂起
事件循环进入 “等待状态”,直到有任务可唤醒
时间线推进,唤醒task2:
T+1 秒(从task2挂起开始计时):
定时器到期,事件循环唤醒task2;
执行print('task2 结束') → 控制台输出:task2 结束;
task2返回 200,协程完成;
此时事件循环中仅剩task1(挂起状态,等待future结果)。
子线程完成thread_task,唤醒sub_task:
T+5 秒(从thread_task启动开始计时):
子线程执行time.sleep(5),进入futrue.set_result(100) → 将future的状态改为done,结果设为 100;
事件循环检测到future完成,唤醒sub_task;
result = await future → 获取future的结果 100;
执行print('sub task 结束') → 控制台输出:sub task 结束;
sub_task返回 100,协程完成。
task1恢复执行:
await sub_task() 获取结果 100,赋值给result;
执行print('task1 结束') → 控制台输出:task1 结束;
task1返回 100,协程完成。
main协程完成:
asyncio.gather收集到task1(100)、task2(200)的结果,返回列表[100, 200];
执行print(result) → 控制台输出:[100, 200];
main协程完成,事件循环终止。
补充说明
线程池的作用:thread_task是阻塞任务(time.sleep(5)),如果直接在协程中执行会阻塞事件循环;通过run_in_executor提交到线程池,让阻塞任务在子线程执行,事件循环可继续调度其他协程。
Future 对象的作用:作为「结果容器」,连接子线程和协程:子线程完成后设置future结果,协程通过await future获取结果并恢复执行。
运行到result = await sub_task()时会不会将task1挂起,调度task2运行?
当task1运行到result = await sub_task()时:
sub_task()会创建sub_task协程对象,await会立即触发sub_task协程的执行(而非直接挂起task1);
事件循环会先执行sub_task内部的逻辑:打印sub task 开始 → 创建 future → 提交线程任务到线程池。
sub_task内部的await future才是真正的 “挂起点”:
future此时处于pending状态(线程任务刚提交,还在执行time.sleep(5),未调用set_result);
await 未完成的future会触发sub_task协程挂起,并将控制权交还给事件循环;
由于task1正在await sub_task()的完成,sub_task挂起后,task1也随之进入挂起状态(因为task1的执行依赖sub_task的结果)
当task1(及内部的sub_task)都处于挂起状态时,事件循环的 “活跃任务队列” 中已无可执行的协程,此时会调度队列中等待的task2协程执行。
| 代码行 | 运行结果 |
|---|---|
result = await sub_task() |
触发sub_task协程执行(不直接挂起 task1) |
sub_task内await future |
sub_task挂起 → 导致task1挂起 → 事件循环调度task2运行 |
总结:
| 条件 | 是否阻塞事件循环 | 能否调度 task2 |
|---|---|---|
| 原代码(future + 线程池 + await) | ❌ 不阻塞 | ✅ 能调度 |
| 去掉手动 future + 保留线程池 + await | ❌ 不阻塞 | ✅ 能调度 |
| 去掉线程池 + 直接执行阻塞任务 | ✅ 阻塞 | ❌ 不能调度 |
| 保留 future / 线程池 + 去掉 await | ✅ 阻塞 | ❌ 不能调度 |
run_in_executor本身会返回一个Future对象(无需手动创建),await 这个future依然是有效的异步挂起点
async def sub_task():
print('sub task 开始')
event_loop = asyncio.get_running_loop()
executor = ThreadPoolExecutor()
# 去掉手动future,直接await run_in_executor的返回值(内置future)
result = await event_loop.run_in_executor(executor, lambda: (time.sleep(5), 100)[1])
print('sub task 结束')
return result
去掉 future + 去掉线程池,直接执行阻塞任务(阻塞事件循环,task2 无法调度):
time.sleep(5)是同步阻塞,且在事件循环的主线程执行
事件循环线程被卡住,无法切换到 task2,直到 5 秒后sub_task/task1执行完,才会调度 task2
async def sub_task():
print('sub task 开始')
# 去掉future、去掉线程池,直接执行阻塞任务
time.sleep(5) # 事件循环线程执行sleep,直接阻塞
print('sub task 结束')
return 100
仅去掉 await future(保留 future 和线程池),仍会阻塞:
如果保留 future 和线程池,但去掉await future,改为同步等待
虽然阻塞任务在子线程执行,但while not future.done()是同步轮询,占用事件循环线程;
没有await这个 “协作式挂起点”,事件循环无法切换到 task2,本质还是「事件循环线程被阻塞」。
async def sub_task():
print('sub task 开始')
event_loop = asyncio.get_running_loop()
future = event_loop.create_future()
executor = ThreadPoolExecutor()
event_loop.run_in_executor(executor, lambda: (time.sleep(5), future.set_result(100))[1])
# 去掉await,改为同步等待future结果(阻塞事件循环)
while not future.done():
pass
result = future.result()
print('sub task 结束')
return result
总结:future不是 “非阻塞” 的核心,它只是 “结果容器”;

浙公网安备 33010602011771号