0
点赞
收藏
分享

微信扫一扫

Python实现协程(一)

舍予兄 2021-09-28 阅读 57

Python 生成器中用到的 yield item 具有 2 个含义:产出和让步。

yield item 这行代码会产出一个值,提供给 next() 调用方。此外还会做出让步,即暂停执行生成器,让调用方继续工作,直到需要使用另一个值时,才会回到生成器上次退出的地方继续执行。

从句法上看,协程与生成器类似,都是包含 yield 关键字的函数。但在协程中,yield 表达式通常为: data = yield。可以产出值,也可以不产出(如果 yield 关键字后面没有表达式,那么生成器产出 None)。

此外,协程通常会从调用方接收数据,调用方把数据提供给协程使用 .send(data) 方法。yield 关键字甚至还可以不接收或传出数据。

不管数据如何流动,yield 都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其它的协程。

一. 协程的概念

协程(Coroutine),又称微线程,纤程,但协程本质上是一个线程在运行。线程比进程轻量,而协程比线程还要轻量。多线程在同一个进程中运行,而协程通常也在在同一个线程中运行。

由于 CPython 解析器的 GIL 原因,多线程的效率受到了很大制约,并且在类 *inux 系统中,创建线程的开销并不比进程小。

后来人们发现通过 yield 来中断代码片段的执行, 同时交出了 CPU 的使用权,于是协程的概念产生了,并在 Python3.4 中正式引入。协程通过应用程序,记录上下文栈区,实现在程序执行过程中的跳跃执行。由此可以选择不阻塞的部分执行以提升运行效率。

和多线程相比,协程具有如下优点:

  • 线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度;
  • 资源消耗少,无需多线程那样进行多核间的切换;
  • 无需同步互斥操作;
  • 没有 C10K 问题,IO 并发性好,一个 CPU 支持上万的协程都不是问题,所以很适合用于高并发处理。

协程的缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将单个 CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是 CPU 密集型应用。
  • 进行阻塞(Blocking)操作(如 IO 时)会阻塞掉整个程序。

二. 使用yield实现协程

2.1 用作协程的生成器的基本行为

生成器 API 新增的方法 / 特性 说明
send(value) 调用方可以使用 .send() 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。
throw(...) 调用方抛出异常,并在生成器中处理。
close() 终止生成器。
return 生成器可以使用 return 返回一个值。
yield from 把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器工作委托给子生成器所需的大量样板代码。

使用生成器函数定义协程的例子:

def simple_coroutine():
    print('-> coroutine started')
    x = yield
    print('-> coroutine received:', x)
 
 
coro = simple_coroutine()
next(coro)
coro.send('this message from caller.')

运行结果:示例中 yield 关键字右边没有表达式,所以该协程只从调用者那里接受数据,yield 产出返回给调用者的值为 None ,即 next(coro) 获取的是 None

调用包含 yield 关键字的函数 simple_coroutine 之后,得到一个生成器对象 coro。此时生成器还没启动,没在 yield 语句处暂停,所以一直无法发送数据。

调用 next 方法以后,协程定义体中的 yield 方法被挂起,控制权回到调用方,执行 coro.send(...) 后,协程会恢复,一直运行到下一个 yield 表达式或终止。

此是,yield 表达式将 this message form caller 赋值给 x ,控制权流动到协程定义体的末尾,导致生成器抛出 StopIteration 异常。

协程可身处下面 4 个状态中的一个,当前状态可使用 inspect.getgeneratorstate(...) 函数获取,该函数返回的字符串及含义如下:

inspect.getgeneratorstate(...) 函数返回值 含义
GEN_CREATED 等待开始执行
GEN_RUNNING 解释器正在执行。只有在多线程应用中才能看到这个状态。
GEN_SUSPENDED yield 表达式处暂停。因为 send 方法的参数会作为 yield 表达式的值,所以,仅当程序处于暂停状态时才能调用 send 方法。
GEN_CLOSED 执行结束。

一个简单的例子来说明生成器(协程)的状态:

from inspect import getgeneratorstate
from time import sleep
 
 
def corotine():
    for i in range(3):
        sleep(0.5)
        x = yield i+1
        print('-> corotine x=', x)
 
 
coro = corotine()
while True:
    try:
        print(f'corotine status:{getgeneratorstate(coro)}')
        coro.send(100)
        next(coro)
        print(f'corotine status:{getgeneratorstate(coro)}')
        next(coro)
        print(f'corotine status:{getgeneratorstate(coro)}')
    except TypeError as e:
        print(e)
        coro = corotine()
        next(coro)  # 激活生成器
        continue
    except StopIteration:
        print('coroutine is finished.')
        print(f'corotine status:{getgeneratorstate(coro)}')
        break
 

运行结果:进入循环后,生成器处于 GEN_CREATED 状态,尚未激活,因此 send(100) 将触发 TypeError 异常,异常信息非常清晰。

最先调用 next(coro) 函数这一步通常视为激活协程,即让协程向前执行至第一个 yield 表达式,准备好作为活跃的协程使用。

下面再举个例子,该示例定义一个产出两个值的协程:

from inspect import getgeneratorstate
 
 
def sample_coro2(a):
    print('-> Started: a=', a)
    b = yield a
    print('-> Received: b=', b)
    c = yield a + b
    print('-> Received: c=', c)
 
 
coro = sample_coro2(2)
try:
    print(getgeneratorstate(coro))
    print('调用方:', next(coro))
    print(getgeneratorstate(coro))
    print('调用方:', coro.send(4))
    print('调用方:', coro.send(8))
except StopIteration:
    print(getgeneratorstate(coro))

运行结果:协程函数调用时并不会立即执行,所以首先打印的是 GEN_CREATED ,当执行 next(coro) 后,协程被激活,开始运行,执行到 b=yield a 后,暂停协程,让步 CPU 的使用权给调用方。

调用方此刻打印 coro 的状态为 GEN_SUSPENDED 状态,即协程在 yield 表达式处暂停状态。

继续向下执行 coro.send(4) ,又将调用权给到协程,协程表达式 yield a 的返回值即 coro.send(4) 发送的4,因此打印 b=4

协程继续向下执行到 c=yield a+b ,再次将 CPU 的使用权让步给调用方,调用方获取到 yield a+b 产出的值 6

当协程函数执行完毕,将抛出 StopIteration 异常,调用方捕获此异常,打印 coro 的状态发现,协程已处于 GEN_CLOSED 状态。

2.2 一个协程应用的例子

下面我们介绍一个关于协程略微复杂的例子:使用协程计算平均值。

def average():
    total = 0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count

这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。仅当调用方在协程上调用 .close() 方法,或者没有协程的引用而被垃圾回收程序回收时,这个协程才会终止。

这里的 yield 表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无限循环。

使用协程的好处是,totalcount 声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保存上下文。

运行结果:调用 next(coro_ava) 函数后,协程会向前执行到 yield 表达式,产出 average 变量的初始值 None,因此不会出现在控制台中。

此时,yield 在协程表达式处暂停,等待调用方发送值。 coro_ava.send(10) 发送一个值,激活协程,把发送的值赋给 term ,并更新 totalcountaverage 三个变量的值,然后开始 while 循环的下一次迭代,产出 average 变量的值,等待下一次为 term 变量赋值。

举报

相关推荐

0 条评论