31. 协程的使用
一、什么是协程
从 Python 3.4 开始,Python 加入了协程的概念,使用 asyncio 模块实现协程。但这个版本的协程还是以生成器对象为基础。 Python 3.5 中增加了 async、await 关键字,使协程的实现更加方便。
协程(Coroutine),又称 微线程,是一种运行运行在用户态的轻量级线程。协程可以在单线程的情况下实现并发。
协程拥有自己的寄存器上下文和栈。协程在调度切换时,将寄存器下上文和栈保存到其它地方,等切换回来的时候,再恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入,就相当于进入上一次调用的状态。
协程本质上是个 单进程,相对于多进程来说,它没有线程上下文切换的开销,没有原子操作锁定及同步的开销。
关于协程,我们还需要了解以下几个概念:
event_loop:事件循环,相当于无限循环,我们可以把一些函数注册到这个事件循环上,当满足发生条件的时候,就调用对应的处理方法。coroutine:在 Python 中常指代 协程对象类型,我们可以 将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async关键字来定义一个方法,这个方法在调用不会立即被执行,而是会返回一个 协程对象。task:任务,这是 协程对象的进一步封装,包含协程对象的各个状态。future:代表 将来执行或者没有执行的任务的结果,实际上和 task 没有本质区别。
协程函数 就是使用 async 关键字修饰的函数。协程对象 就是 执行协程函数得到的对象。执行协程函数时,协程函数内部的代码不会执行。如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理。当协程对象传递给 run_until_complete() 方法时,实际它将 coroutine 封装成 task 对象。
事件循环 可以理解为一个死循环,它循环检测并执行某些代码,它的实现原理如下:
任务列表 = [任务1, 任务2, 任务3, ...]
while True:
可执行的任务列表, 已完成的列表列表 = 去任务列表中检查所有的任务,将“可执行”和“已完成”的任务返回
for 就绪任务 in 可执行的任务列表:
执行已就绪的任务
for 已完成的任务 in 已完成的任务列表:
在任务列表中移除“已完成”的任务
if 任务列表中的任务都已完成:
break
在 Python 3.7 之后,得到 协程对象对象 之后,我们可以使用 asyncio.run() 方法 执行协程对象。它方法会阻塞当前线程,直到任务执行完毕,然后将任务结果返回。
import asyncio
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(name):
print(f"【{name}】开始执行!")
print(f"【{name}】执行完毕!")
return f"【{name}】的返回值"
if __name__ == '__main__':
coroutine_object = work("任务1") # 获取协程对象
result = asyncio.run(coroutine_object) # 运行协程对象
print(result) # 打印协程函数的返回值
要实现 异步处理,先要有 挂起操作,当一个任务需要等待 I/O 结果的时候,可以挂起当前任务,转而执行其它任务,这样才能充分利用好资源。
await 关键字可以 将耗时等待的操作挂起,让出控制权。如果协程在执行的时候遇到 await,事件循环就会将本协程挂起,转而执行别的协程,直到其它协程挂起或执行完毕。
await 关键字的主要作用如下:
- 挂起:
await会 暂停当前协程的执行。 - 等待:遇到
await关键字,事件循环会立即安排await后面的对象去执行,并等待该对象执行完成,并且可以拿到执行结果。- 如果在执行该对象中的代码中,遇到了
await I/O操作(需要等待外部资源返回结果的操作),那 CPU 的控制权就会交给事件循环,事件循环会去调度循环中的其它任务(如果有其它任务的话)。 - 如果该对象中的代码,不包含任何
await I/O操作,此时事件循环拿不到 CPU 的控制权,无法调度循环中的其它任务,不会发生任务切换。
- 如果在执行该对象中的代码中,遇到了
- 恢复:当
await后面的对象执行完毕,事件循环会恢复之前被挂起的协程,该协程会从当时挂起的位置继续执行,并拿到返回值。
await 后面的对象必须是 可等待的对象,常见的可等待对象如下:
-
协程(coroutine):由
async def定义的协程函数返回的 协程对象。 -
任务(Task):通过
asyncio.create_task()等创建的,是Future的子类。 -
Future(未来对象):asyncio.Future,代表一个异步操作的最终结果。 -
实现
__await__方法,并且该方法返回的一个 迭代器 的对象。 -
一个由
types.coroutine修饰的 生成器,这个生成器可以返回 协程对象。
import asyncio
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(name, delay):
print(f"【{name}】开始执行!")
# 遇到I/O操作挂起当前协程(任务),等待I/O操作完成之后再继续往下执行
# 当前协程挂起时,事件循环可以去执行其它任务
await asyncio.sleep(delay)
print(f"【{name}】执行完毕!")
return f"【{name}】的返回值"
async def main():
print("开始执行main()函数内部代码!")
result = await work("任务1", 3)
print(result)
print("main()函数内部代码执行完毕!")
return "main()函数的返回值"
if __name__ == '__main__':
result = asyncio.run(main())
print(result)
如果我们直接使用 await 多个协程对象会发现代码是同步执行的,即 work1 先执行完毕之后,work2 在开始执行,依次下去。这时因为,当代码执行到 await coroutine1 时,CPU 的控制权就会交给事件循环,事件循环会去调度循环中的其它任务,但此时事件循环中没有其它可调度的任务,因此会空等待下去,直到该任务执行完毕。
import time
import asyncio
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(name, delay):
print(f"【{name}】开始执行!")
# 遇到I/O操作挂起当前协程(任务),等待I/O操作完成之后再继续往下执行
# 当前协程挂起时,事件循环可以去执行其它任务
await asyncio.sleep(delay)
print(f"【{name}】执行完毕!")
return f"【{name}】的返回值"
async def main():
print("开始执行main()函数内部代码!")
start = time.time()
# 获取协程对象
coroutine1 = work("任务1", 3)
coroutine2 = work("任务2", 3)
coroutine3 = work("任务3", 3)
result1 = await coroutine1
print(result1)
result2 = await coroutine2
print(result2)
result3 = await coroutine3
print(result3)
end = time.time()
print(f"耗时:{end - start}")
print("main()函数内部代码执行完毕!")
return "main()函数的返回值"
if __name__ == '__main__':
result = asyncio.run(main())
print(result)
此时,我们可以使用 asyncio.create_task() 方法把一个 协程对象包装成可被事件循环调度的任务,并 注册到事件循环中。当代码执行到 await task1 时,CPU 的控制权就会交给事件循环,事件循环会去调度循环中的其它任务,此时事件循环中还有 task2 任务和 task3 任务可以调度。同理,当 task2 任务挂起时,事件循环中还有 task3 任务可调动。task3 任务被挂起后,由于事件循环中没有其它可调度的任务,会进入等待状态,直至 task1 任务 I/O 操作结束,结束挂起。然后事件循环会重新调度 task1,依次类推。
import time
import asyncio
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(name, delay):
print(f"【{name}】开始执行!")
# 遇到I/O操作挂起当前协程(任务),等待I/O操作完成之后再继续往下执行
# 当前协程挂起时,事件循环可以去执行其它任务
await asyncio.sleep(delay)
print(f"【{name}】执行完毕!")
return f"【{name}】的返回值"
async def main():
print("开始执行main()函数内部代码!")
start = time.time()
# 获取协程对象
coroutine1 = work("任务1", 3)
coroutine2 = work("任务2", 3)
coroutine3 = work("任务3", 3)
# 创建任务对象
task1 = asyncio.create_task(coroutine1)
task2 = asyncio.create_task(coroutine2)
task3 = asyncio.create_task(coroutine3)
result1 = await task1
print(result1)
result2 = await task2
print(result2)
result3 = await task3
print(result3)
end = time.time()
print(f"耗时:{end - start}")
print("main()函数内部代码执行完毕!")
return "main()函数的返回值"
if __name__ == '__main__':
result = asyncio.run(main())
print(result)
当我们有多个协程对象时,我们可以使用 asyncio.gather() 方法一次 将多个协程对象同时丢给事件循环,并在全部执行完毕之后,一次拿到所有的结果。
import time
import asyncio
# 协程函数,定义函数的时候,使用async关键字修饰的函数
async def work(name, delay):
print(f"【{name}】开始执行!")
# 遇到I/O操作挂起当前协程(任务),等待I/O操作完成之后再继续往下执行
# 当前协程挂起时,事件循环可以去执行其它任务
await asyncio.sleep(delay)
print(f"【{name}】执行完毕!")
return f"【{name}】的返回值"
async def main():
print("开始执行main()函数内部代码!")
start = time.time()
# 获取协程对象
coroutine1 = work("任务1", 3)
coroutine2 = work("任务2", 3)
coroutine3 = work("任务3", 3)
# 创建任务对象
results = await asyncio.gather(coroutine1, coroutine2, coroutine3)
for result in results:
print(result)
end = time.time()
print(f"耗时:{end - start}")
print("main()函数内部代码执行完毕!")
return "main()函数的返回值"
if __name__ == '__main__':
result = asyncio.run(main())
print(result)

浙公网安备 33010602011771号