python 的协程

2019-04-30 12:25:11   最后更新: 2019-04-30 12:25:11   访问数量:95




上一篇文章中,我们介绍了 Python 中的 yield 关键字以及依赖其实现的生成器函数

python 中的迭代器与生成器

 

生成器函数在形式上与协程已经十分接近,本文我们就来详细介绍一下协程

 

协程又称为微线程,虽然整个执行过程中只有一个线程,但某个方法的执行过程中可以挂起、让出CPU给另一个方法,等到适当的时机再回到原方法继续执行,但两个方法之间并没有相互调用关系,他们类似于系统中断或多线程的表现

由此,我们可以看到协程具有以下优势:

  1. 执行效率高,通过执行中的切换,让多个方法近乎同时执行,减少IO等待,有效提升了执行效率
  2. 性能优于多线程,对于多线程并发的程序设计,多个线程切换过程中需要消耗一定的时间,而协程切换的时间消耗则十分微小,并且随着并发量越大优势越明显
  3. 编程相对简单,因为协程中的多个方法均在同一个线程中,所以协程中没有竞争条件,不需要考虑加锁

 

示例

>>> def simple_coroutine(): ... print('-> coroutine started') ... x = yield ... print('-> coroutine received:', x) ... >>> my_coro = simple_coroutine() >>> my_coro <generator object simple_coroutine at 0x100c2be10> >>> next(my_coro) -> coroutine started >>> my_coro.send(42) -> coroutine received: 42 Traceback (most recent call last): ... StopIteration

 

 

可以看到,在上面的例子中,yield 不再是我们所熟悉的出现在式子的左边,而是成为了变量赋值的右值,事实上,此处 yield 右侧同样可以出现值、变量或表达式

当程序执行到 yield 表达式时,协程被挂起,同时返回 yield 右侧的值(如果有的话)

对这个协程执行 send 操作实际上就是将 send 方法的参数传递给 yield 表达式的左值,接着程序继续运行下去

 

协程有以下四种状态:

  1. GEN_CREATED -- 等待开始执行
  2. GEN_RUNNING -- 正在执行
  3. GEN_SUSPENDED -- 在 yield 表达式处暂停
  4. GEN_CLOSED -- 执行结束

 

通过使用 inspect.getgeneratorstate 函数可以返回上述四个中的一个状态字符串

只有当一个协程处于 GEN_SUSPENDED 状态时才可以调用其 send 方法,否则会抛出异常:

>>> my_coro = simple_coroutine() >>> my_coro.send(1729) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't send non-None value to a just-started generator

 

 

协程的执行

>>> def simple_coro2(a): ... print('-> Started: a =', a) ... b = yield a ... print('-> Received: b =', b) ... c = yield a + b ... print('-> Received: c =', c) ... >>> my_coro2 = simple_coro2(14) >>> from inspect import getgeneratorstate >>> getgeneratorstate(my_coro2) 'GEN_CREATED' >>> next(my_coro2) -> Started: a = 14 14 >>> getgeneratorstate(my_coro2) 'GEN_SUSPENDED' >>> my_coro2.send(28) -> Received: b = 28 42 >>> my_coro2.send(99) -> Received: c = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> getgeneratorstate(my_coro2) 'GEN_CLOSED'

 

 

下图展示了上述代码的执行过程:

 

 

预激

因此需要首先调用 next 方法,让协程执行到第一个 yield 表达式,这一过程被称为“预激”(prime)

所有协程都必须预激然后使用,这一次 next 调用看上去总是让人觉得有些多余,而没有他又会报错,所以我们可以在自己的协程上加一个装饰器,以使协程被创建后自动完成预激功能

from functools import wraps def coroutine(func): @wraps(func) def primer(*args,**kwargs): gen = func(*args,**kwargs) next(gen) return gen return primer

 

 

关于装饰器的内容,可以参考:

python 中的装饰器及其原理

 

关闭

有下面几种情况会让协程进入 GEN_CLOSED 状态:

  1. 与迭代器、生成器函数一样,当我们不断执行 next 方法或 send 方法让所有 yield 表达式依次被执行,直到最后一个 yield 表达式被执行后,就会抛出 StopIteration 异常,此时协程进入 GEN_CLOSED 状态
  2. 协程同时提供了 close 方法,无论协程处于什么状态,close 方法可以立即让协程进入 GEN_CLOSED 状态
  3. 如果协程运行中出现未捕获异常,异常首先会传递给 next 或 send 方法抛出,协程也将终止
  4. 你也可以调用 throw 方法主动将一个异常传递给协程并抛出,达到让协程抛出异常并关闭协程的目的,事实上 close 方法也是通过让协程抛出 GeneratorExit 异常实现的
  5. 还有一种情况会使协程进入 GEN_CLOSED 状态,那就是当没有任何引用指向他时被回收

 

关于 Python 的垃圾回收机制,参考:

python 的内存管理与垃圾收集

 

示例 -- 利用协程计算移动平均数

from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total/count

 

 

可以看到,上例中,协程是一个无限循环,只要调用方不断将值发送给协程,他就会不断累加、计算移动平均数,直到协程的 close 方法被调用或协程对象被垃圾回收

 

下面的例子中,我们在上面计算移动平均数的代码最后加上了返回语句

from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total/count return Result(count, average) >>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> coro_avg.send(None) Traceback (most recent call last): ... StopIteration: Result(count=3, average=15.5)

 

 

可以看到,在最终给协程发送 None 导致协程退出后,抛出的 StopIteration 中携带了这个返回值,通过 StopIteration 的 value 字段我们可以取出该值:

 

yield from 语句可以简化生成器函数中的 yield 表达式,这在我们此前的文章中已经介绍过:

>>> def gen(): ... for c in 'AB': ... yield c ... for i in range(1, 3): ... yield i ... >>> list(gen()) ['A', 'B', 1, 2]

 

 

可以改写成:

>>> def gen(): ... yield from 'AB' ... yield from range(1, 3) ... >>> list(gen()) ['A', 'B', 1, 2]

 

 

包含 yield from 语句的函数被称为委派生成器,他打开了双向通道,将最外层的调用方与最内层的子生成器连接起来,让二者可以直接发送和产出值,还可以直接传入异常,位于中间的协程无序添加任何中间处理的代码

yield from 语句会一直等待子生成器终止并抛出 StopIteration 异常,而子生成器通过 return 语句返回的值会成为 yield from 语句的传入值

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

 

 






python      并发      调度      装饰器      yield      协程     


京ICP备15018585号