31. 协程的使用
一、什么是协程
从 Python 3.4 开始,Python 加入了协程的概念,使用 asyncio 模块实现协程。但这个版本的协程还是以生成器对象为基础。 Python 3.5 中增加了 async、await 关键字,使协程的实现更加方便。
协程(Coroutine),又称 微线程,是一种运行运行在用户态的轻量级线程。协程可以在单线程的情况下实现并发。
协程拥有自己的寄存器上下文和栈。协程在调度切换时,将寄存器下上文和栈保存到其它地方,等切换回来的时候,再恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入,就相当于进入上一次调用的状态。
协程本质上是个单进程,相对于多进程来说,它没有线程上下文切换的开销,没有原子操作锁定及同步的开销。
关于协程,我们还需要了解以下几个概念:
- event_loop:事件循环,相当于无限循环,我们可以把一些函数注册到这个事件循环上,当满足发生条件的时候,就调用对应的处理方法;
- coroutine:在 Python 中常指代协程对象类型,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用不会立即被执行,而是会返回一个协程对象;
- task:任务,这是协程对象的进一步封装,包含协程对象的各个状态;
- future:代表将来执行或者没有执行的任务的结果,实际上和 task 没有本质区别;
二、协程的使用
import asyncio
import time
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(x):
print(f"work({x}) start!")
print(f"当前任务的参数为:{x}")
# 遇到IO操作挂起当前协程(任务),等待IO操作完成之后再继续往下执行
# 当前协程挂起时,事件循环可以去执行其它任务
await asyncio.sleep(x)
print(f"work({x}) end!")
return f"当前任务的返回值:{x}"
async def main():
print("开始执行main()函数内部代码!")
tasks = [
# asyncio.create_task()创建一个task对象
asyncio.create_task(work(2)),
# asyncio.ensure_future()创建一个task对象
asyncio.ensure_future(work(3)),
asyncio.create_task(work(5)),
]
# asyncio.wait()接收的参数为task对象,返回一个二值元组
# done接收任务的返回值,pending接收任务的状态
done, pending = await asyncio.wait(tasks)
for item in done:
print(item)
print("main()函数内部代码执行完毕!")
if __name__ == "__main__":
start = time.time()
# 协程对象,执行协程函数()得到的协程对象
# 执行协程函数创建协程对象,函数内部的代码不会执行
result = main()
# 去生成或获取一个事件循环
# loop = asyncio.get_event_loop()
# 将任务放到任务列表中
# loop.run_until_complete(result)
# Python 3.7之后的版本可以简化为如下代码
asyncio.run(result)
print(time.time() - start)
协程函数 就是使用 async 关键字修饰的函数。协程对象 就是执行协程函数得到的对象。执行协程函数时,协程函数内部的代码不会执行。如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理。当我们把协程对象传递给 run_until_complete() 方法时,实际它将 coroutine 封装成 task 对象。
事件循环 可以理解为一个死循环,它循环检测并执行某些代码,它的实现原理如下:
任务列表 = [任务1, 任务2, 任务3, ...]
while True:
可执行的任务列表, 已完成的列表列表 = 去任务列表中检查所有的任务,将“可执行”和“已完成”的任务返回
for 就绪任务 in 可执行的任务列表:
执行已就绪的任务
for 已完成的任务 in 已完成的任务列表:
在任务列表中移除“已完成”的任务
if 任务列表中的任务都已完成:
break
要实现异步处理,先要有挂起操作,当一个任务需要等待 IO 结果的时候,可以挂起当前任务,转而执行其它任务,这样才能充分利用好资源。
await 关键字可以将耗时等待的操作挂起,让出控制权。如果协程在执行的时候遇到 await,事件循环就会将本协程挂起,转而执行别的协程,直到其它协程挂起或执行完毕。
await 后面的对象必须是如下格式之一:
- 一个原生的协程对象;
- 一个由 types.coroutine 修饰的生成器,这个生成器可以返回协程对象;
- 有一个包含
__await__
方法的对象返回的一个迭代器;
三、手动实现协程
进程、线程创建完之后,到底是哪个进程、线程执行去执行,这是不确定的,这需要有操作系统来进行计算(调度算法,例如优先级调度)。而协程是可以人为来控制的。程序员在代码层面上检测所有的 IO 操作,一旦遇到 IO 操作,程序员在代码级别完成切换,这样给 CPU 的感觉就是这个程序一直运行,没有 IO 操作,从而提升程序的运行效率。
3.1、使用greenlet模块实现协程
我们可以使用 greenlet 模块实现协程,但遇到 IO(指的是 input output,输入输出,比如,网络、文件操作等) 操作时,还需要人工切换。
我们可以在终端中使用 pip 命令安装 greelet 模块:
pip install greenlet
import time
from greenlet import greenlet
def task1():
while True:
print("task1 work!")
g2.switch()
time.sleep(0.5)
def task2():
while True:
print("task2 work!")
g1.switch()
time.sleep(0.5)
if __name__ == "__main__":
g1 = greenlet(task1)
g2 = greenlet(task2)
g1.switch()
3.2、使用gevent模块实现协程
greenlet 模块已经实现了协程,但还需要人工切换。Python 还提供了一个能自动切换任务的 gevent 模块。其原理是当一个 greenlet 遇到 IO 就会自动切换到其它的 greenlet 再执行,而不是等待 IO。
我们可以在终端中使用 pip 命令安装 gevent 模块:
pip install gevent
import time
from gevent import spawn
# gevent模块本身无法检测常见的一些IO操作
# 在使用的时候需要额外导入monkey模块
from gevent import monkey
monkey.patch_all()
def say_hello():
print("hello")
time.sleep(2)
print("hello")
def say_hi():
print("hi")
time.sleep(3)
print("hi")
def say_good():
print("good")
time.sleep(5)
print("good")
start_time = time.time()
g1 = spawn(say_hello)
g2 = spawn(say_hi)
g3 = spawn(say_good)
# 等待被检测的任务执行完毕,再往后执行
g1.join()
g2.join()
g3.join()
print(time.time() - start_time)
import gevent
import time
# gevent模块本身无法检测常见的一些IO操作
# 在使用的时候需要额外导入monkey模块
from gevent import monkey
monkey.patch_all()
def say_hello():
print("hello")
time.sleep(2)
print("hello")
def say_hi():
print("hi")
time.sleep(3)
print("hi")
def say_good():
print("good")
time.sleep(5)
print("good")
start_time = time.time()
gevent.joinall([
gevent.spawn(say_hello),
gevent.spawn(say_hi),
gevent.spawn(say_good),
])
print(time.time() - start_time)
time 模块的 sleep() 方法不具备自动切换任务的功能,而 gevent 模块的 sleep() 方法具有该能功能,所以我们使用猴子补丁将 time 模块的 sleep() 方法编程 gevent 模块的 sleep() 方法;