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)

image

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()

image

关于以上代码:
  协程函数的返回值不是函数运行的结果,而是一个协程对象。
  协程对象是一个未开始运行的任务。要运行这个任务,必须通过事件循环来调度

[4] 核心概念

方面异步函数 (Async Function)协程 (Coroutine)
定义 用 async def 定义的函数,是「协程(协程对象)的创建器」 异步函数调用后返回的可等待对象,是「可被await调度运行的异步任务单元」
类型 function 类型 coroutine 类型
调用 调用时返回协程(协程对象) 本身就是可等待对象
关系 异步函数是创建协程(协程对象)的工具,调用后才生成协程对象 异步函数的实例
复用 可多次调用 通常只能被 await 一次
await                 await 等待的是协程(协程对象)而非异步函数
一句话概括:调用异步函数生成协程(协程对象) 

[5] 事件循环

(1) 概念

事件循环可以类比while循环来理解,在循环周期内运行一些任务,特定的条件下结束循环。

import asyncio

loop = asyncio.get_event_loop()  # 完整版,另外有简化版本
(2)使用事件循环调用协程函数
import asyncio

async def work():
    print(666)

loop = asyncio.run(work())  # 简化版

image

 

import asyncio

async def f1():
    print('hello')

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

if __name__ == '__main__':
    work()

image

 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() 会自动创建和关闭事件循环,简化了异步代码的编写。

(2) 方法二(简化版):
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}')

image 

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 秒(最长的那个任务的耗时)。

image 

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}')

image

用法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}')

image

 用法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():调用协程函数仅创建「协程对象」,不会立即执行,需由事件循环调度。

事件循环将task1task2包装为「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收集到task1100)、task2200)的结果,返回列表[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_taskawait future sub_task挂起 → 导致task1挂起 → 事件循环调度task2运行

总结:

协程只会在明确的挂起点(await 未完成的可等待对象)交出控制权,而非一遇到 await 就挂起。
 
 
有无future讨论
条件是否阻塞事件循环能否调度 task2
原代码(future + 线程池 + await) ❌ 不阻塞 ✅ 能调度
去掉手动 future + 保留线程池 + await ❌ 不阻塞 ✅ 能调度
去掉线程池 + 直接执行阻塞任务 ✅ 阻塞 ❌ 不能调度
保留 future / 线程池 + 去掉 await ✅ 阻塞 ❌ 不能调度
去掉手动创建的 future,但保留线程池(仍能调度 task2):

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不是 “非阻塞” 的核心,它只是 “结果容器”;

 

 

posted @ 2024-10-04 22:20  pythondjango  阅读(90)  评论(0)    收藏  举报