31. 协程的使用

一、什么是协程

  从 Python 3.4 开始,Python 加入了协程的概念,使用 asyncio 模块实现协程。但这个版本的协程还是以生成器对象为基础。 Python 3.5 中增加了 asyncawait 关键字,使协程的实现更加方便。

  协程(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 关键字的主要作用如下:

  1. 挂起await 会 暂停当前协程的执行
  2. 等待:遇到 await 关键字,事件循环会立即安排 await 后面的对象去执行,并等待该对象执行完成,并且可以拿到执行结果
    • 如果在执行该对象中的代码中,遇到了 await I/O 操作(需要等待外部资源返回结果的操作),那 CPU 的控制权就会交给事件循环,事件循环会去调度循环中的其它任务(如果有其它任务的话)。
    • 如果该对象中的代码,不包含任何 await I/O 操作,此时事件循环拿不到 CPU 的控制权,无法调度循环中的其它任务,不会发生任务切换。
  3. 恢复:当 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)
posted @ 2024-11-13 18:12  星光映梦  阅读(109)  评论(0)    收藏  举报