一文讲透 python 协程

1. 引言

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

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

2. 协程

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

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

2.1. 示例

>>> 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 表达式的左值,接着程序继续运行下去。

3. 协程的状态

协程有以下四种状态:

  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

3.1. 协程的执行

>>> 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'

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

3.2. 预激

因此需要首先调用 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 中的装饰器及其原理

3.3. 关闭

有下面几种情况会让协程进入 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 的内存管理与垃圾收集

3.4. 示例 — 利用协程计算移动平均数

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 方法被调用或协程对象被垃圾回收。

4. 协程的 return

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

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 字段我们可以取出该值:

5. 委派生成器 — yield from

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 语句的传入值。

6. 微信公众号

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

阅读原文

一文讲透 python 协程》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:https://www.hashtobe.com/365.html