首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一文讲透 python 协程

一文讲透 python 协程

作者头像
用户3147702
发布2022-06-27 13:29:03
发布2022-06-27 13:29:03
8270
举报

1. 引言

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

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

2. 协程

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

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

2.1. 示例

代码语言:javascript
复制
>>> 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 方法,否则会抛出异常:

代码语言:javascript
复制
>>> 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. 协程的执行

代码语言:javascript
复制
>>> 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 调用看上去总是让人觉得有些多余,而没有他又会报错,所以我们可以在自己的协程上加一个装饰器,以使协程被创建后自动完成预激功能。

代码语言:javascript
复制
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. 示例 — 利用协程计算移动平均数

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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 表达式,这在我们此前的文章中已经介绍过:

代码语言:javascript
复制
>>> def gen():
...     for c in 'AB':
...         yield c
...     for i in range(1, 3):
...         yield i
...
>>> list(gen())
['A', 'B', 1, 2]

可以改写成:

代码语言:javascript
复制
>>> def gen():
...     yield from 'AB'
...     yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-01,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 1. 引言
  • 2. 协程
    • 2.1. 示例
  • 3. 协程的状态
    • 3.1. 协程的执行
    • 3.2. 预激
    • 3.3. 关闭
    • 3.4. 示例 — 利用协程计算移动平均数
  • 4. 协程的 return
  • 5. 委派生成器 — yield from
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档