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() 方法;

posted @ 2024-11-13 18:11  星光映梦  阅读(9)  评论(0编辑  收藏  举报