python实现协程(一)

        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实现协程

        本部分使用到的主要方法总结如下:

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

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

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

扫描二维码关注公众号,回复: 10514288 查看本文章
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关键字的函数,得到的是一个生成器对象,首先调用next(coro)函数,因为生成器还没启动,没在yield语句处暂停,所以一直无法发送数据。调用这个方法后,协程定义体中的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表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无限循环。使用协程的好处是,total和count声明为局部变量即可,无需使用实例属性或闭包在多次调用之间保持上下文。

运行结果:调用next(coro_ava)函数后,协程会向前执行到yield表达式,产出average变量的初始值---None,因此不会出现在控制台中。此时,yield在协程表达式处暂停,等待调用方发送值。coro_ava.send(10)发送一个值,激活协程,把发送的值赋给term,并更新total、count、average三个变量的值,然后开始while循环的下一次迭代,产出average变量的值,等待下一次为term变量赋值。

        介绍到这里,细心的你可能迫切想知道如何终止averager实例,因为定义体中有个无限循环。但在讨论如何终止协程之前,我们要先知道如何启动协程。使用协程之前必须预激,可是这一步很容易忘记。下一节,我们将在协程函数上使用一个特殊的装饰器,来预激活协程。

发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/105149380