Python - 协程coroutine
总结
asyncio
里面,await
的用法有两种:
await coroutine
,就像普通的函数调用一样,执行coroutine对应的代码await task
,中断当前代码的执行,event loop开始调度任务,直到task
执行结束,恢复执行当前代码。
前言
常见的Python代码都是一行一行执行的,非常易懂。然而,有时候我们也会在Python代码中看到一些async/await
等与异步编程相关的代码。为了能够顺利读懂这些代码,我们需要了解Python异步编程的一些基础知识。
事实上,Python 3.5就已经开始支持异步编程语法了。从这个角度来看,了解异步编程也是必要的,它早已成为了Python生态里的一部分。
Python中的异步编程的核心语法就是async/await
两个关键字,主要涉及的概念就是协程(coroutine)。关于协程的解释,什么是协程?这篇文章给出了很好的介绍。简单来说,协程就是在一个线程(thread)里通过事件循环(event loop)模拟出多个线程并发的效果。
Python中的协程概念
在Python中,协程coroutine有两层含义:
- 使用
async def
定义的函数是一个coroutine,这个函数内部可以用await
关键字。 - 使用
async def
定义的函数,调用之后返回的值,是一个coroutine对象,可以被用于await
或者asyncio.run
等
我们可以看到:
- 第一层含义是语法层面的概念,一个函数(一段代码)由
async def
定义,那么它就是一个coroutine。带来的效果是,这个函数内部可以用await
。那么反过来就是说,一个普通的def
定义的函数,内部不能用await
,否则就会触发语法错误(SyntaxError)。 - 第二层含义是Python解释器运行时的概念,
coroutine
是Python解释器里内置的一个类。当我们调用async def
定义的函数时,得到的返回值的类型就是coroutine
。
例如下面的代码:
import asyncio
async def hello_world():
await asyncio.sleep(1)
print("Hello, world!")
coro = hello_world()
print(hello_world) # <function hello_world at 0x102a93e20>
print(coro.__class__) # <class 'coroutine'>
asyncio.run(coro) # Hello, world!
从语法层面上来说,hello_world
函数是个coroutine函数。但是运行时,hello_world
函数的类型依然是function
,这个函数调用之后的返回对象coro
是一个coroutine
对象。
await + coroutine
当我们对一个coroutine使用await
时,当前函数中断执行,Python解释器开始执行coroutine的代码,这和普通的函数调用没什么区别:
import asyncio
import time
async def async_hello_world():
now = time.time()
await asyncio.sleep(1)
print(time.time() - now) # 1.0013360977172852
print("Hello, world!") # Hello, world!
await asyncio.sleep(1)
print(time.time() - now) # 2.0025689601898193
print(asyncio.sleep(1)) # <coroutine object sleep at 0x102f663b0>
coro = async_hello_world()
asyncio.run(coro)
由此我们可以看到,asyncio.sleep(1)
是一个coroutine object,对它进行await
就会使得当前coroutine休眠一秒。
虽然这段代码经常被用于展示coroutine的基本用法,但这段代码其实完全没有展现coroutine的优势。我们可以不用coroutine写出功能一致的代码:
import time
def normal_hello_world():
now = time.time()
time.sleep(1)
print(time.time() - now) # 1.0050458908081055
print("Hello, world!") # Hello, world!
time.sleep(1)
print(time.time() - now) # 2.010284900665283
normal_hello_world()
只需要删除所有的async/await
,把asyncio.sleep
换成time.sleep
,就可以了。
那么,我们为什么还要coroutine呢?
其实,coroutine最大的优势在于用单个线程模拟多个线程并发:
import asyncio
import time
async def async_hello_world():
now = time.time()
await asyncio.sleep(1)
print(time.time() - now)
print("Hello, world!")
await asyncio.sleep(1)
print(time.time() - now)
async def main():
await asyncio.gather(async_hello_world(), async_hello_world(), async_hello_world())
now = time.time()
# run 3 async_hello_world() coroutine concurrently
asyncio.run(main())
print(f"Total time for running 3 coroutine: {time.time() - now}")
import time
def normal_hello_world():
now = time.time()
time.sleep(1)
print(time.time() - now)
print("Hello, world!")
time.sleep(1)
print(time.time() - now)
now = time.time()
normal_hello_world()
normal_hello_world()
normal_hello_world()
print(f"Total time for running 3 normal function: {time.time() - now}")
输出为:
1.0004000663757324
Hello, world!
1.0004527568817139
Hello, world!
1.0004589557647705
Hello, world!
2.001703977584839
2.0017318725585938
2.0017342567443848
Total time for running 3 coroutine: 2.0025317668914795
1.005108118057251
Hello, world!
2.010077953338623
1.005120038986206
Hello, world!
2.00691294670105
1.0018260478973389
Hello, world!
2.0058960914611816
Total time for running 3 normal function: 6.0232861042022705
可以看到,一个coroutine执行需要2秒,3个coroutine同时执行还是只需要2秒;而普通的函数,一个函数执行需要2秒,3个函数执行就需要6秒了。
await + task
在Python的异步编程中,真正并发的对象是任务(Task)。当我们对一个Task进行await
的时候,event loop开始调度当前可执行的全部任务,直到被await
的Task结束。
我们可以用Task来模拟asyncio.gather
的效果(事实上,asyncio.gather
内部也是通过Task来实现的):
import asyncio
import time
async def async_hello_world():
now = time.time()
await asyncio.sleep(1)
print(time.time() - now)
print("Hello, world!")
await asyncio.sleep(1)
print(time.time() - now)
async def main():
task1 = asyncio.create_task(async_hello_world())
task2 = asyncio.create_task(async_hello_world())
task3 = asyncio.create_task(async_hello_world())
await task1
await task2
await task3
now = time.time()
# run 3 async_hello_world() coroutine concurrently
asyncio.run(main())
print(f"Total time for running 3 coroutine: {time.time() - now}")
输出为:
1.0012600421905518
Hello, world!
1.0013139247894287
Hello, world!
1.0013208389282227
Hello, world!
2.0027778148651123
2.0029189586639404
2.002932071685791
Total time for running 3 coroutine: 2.0102150440216064
与预期结果一致,三个coroutine执行一共只花了2秒。
如何理解这段代码呢?我们可以使用代码与程序计数器(Program Counter,即PC)来理解:一个Task就是一段将要运行的代码,PC就是当前Task运行到的位置。
当我们运行asyncio.run(main())
时,Python会自动将这个coroutine对象包装成一个Task(记为Task 0)。此时,我们的任务列表长这样:
Task 0执行了3次asyncio.create_task(async_hello_world())
后,我们的任务列表为:
接下来,Task 0开始await task1
,带来的效果为:
- Task 0中断执行,直到
Task 1
结束。于是event loop寻找下一个可以执行的任务(即Task 1) - Task 1执行到
await asyncio.sleep(1)
,也中断执行,等到1秒后再执行。于是event loop寻找下一个可以执行的任务(即Task 2) - Task 2执行到
await asyncio.sleep(1)
,也中断执行,等到1秒后再执行。于是event loop寻找下一个可以执行的任务(即Task 3) - Task 3执行到
await asyncio.sleep(1)
,也中断执行,等到1秒后再执行。于是event loop寻找下一个可以执行的任务。 - 此时没有任务可以执行,于是event loop自己休眠1秒。
- 休眠1秒后,Task 1可以执行,执行了
print(time.time() - now)
与print("Hello, world!")
后,Task 1执行到await asyncio.sleep(1)
,再次中断执行。于是event loop寻找下一个可以执行的任务(即Task 2) - Task 2执行了
print(time.time() - now)
与print("Hello, world!")
后,在await asyncio.sleep(1)
处再次中断执行。于是event loop寻找下一个可以执行的任务(即Task 3) - Task 3同理,执行了
print(time.time() - now)
与print("Hello, world!")
后,在await asyncio.sleep(1)
处再次中断执行。 - 此时没有任务可以执行,于是event loop自己休眠1秒。
- 休眠1秒后,Task 1可以执行,执行了
print(time.time() - now)
之后,Task 1结束。 - Task 0发现Task 1执行结束,于是
await task1
执行结束,开始await task2
。此时Task 0中断执行。 - Task 2执行了
print(time.time() - now)
之后,Task 2结束。 - Task 0发现Task 2执行结束,于是
await task2
执行结束,开始await task3
。此时Task 0中断执行。 - Task 3执行了
print(time.time() - now)
之后,Task 3结束。 - Task 0发现Task 3执行结束,于是
await task3
执行结束,整个Task 0执行结束。
以上流程画成动图会更加生动形象,可惜我不会画动图。如果有感兴趣的朋友可以用一个动图来展示上述流程。
由此我们可以看到,协程最重要的特性,在于多个协程可以同时asyncio.sleep(1)
,现实世界只过去了1秒,而三个协程的时间都过去了1秒,从而节约了等待的时间。
上述分析假设了event loop查询任务状态的顺序为Task 0 --> Task 1 --> Task 2 --> Task 3。实际情况可能是乱序的,例如第6步中,event loop休眠1秒后,Task 1、Task 2、Task 3都可以执行,此时的执行顺序无法预测。同理,第10步中,event loop休眠1秒后,Task 1、Task 2、Task 3都可以执行,谁先执行谁就先结束。我们的await task1
只是保证这段代码必须在task1
结束后继续执行,此时有可能task2
及task3
已经执行结束了。后续的await task2
与await task3
,只是在确保等到task2
及task3
结束。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-10-17 Java 基础 - 错误码规范
2021-10-17 Java 基础 - 自定义枚举类
2021-10-17 SpringBoot - 使用 JSR303 实现参数验证
2019-10-17 Spring - @ManagedResource, @ManagedOperation, @ManagedAttribute
2019-10-17 Spring - <bean parent="xxx" 属性>