Blog地址:https://www.jiangdog.com/blog/yield-and-coroutine
基本概念的认识
之前在浏览相关文章gevent源码分析时对协程和进程做了相关比较。
- 相同点:二者都是可以看做是一种执行流, 该执行流可以挂起,并且在将来又可以在 你挂起的地方恢复执行, 这实际上都可以看做是continuation, 我们来看看当我们挂 起一个执行流时我们要保存的东西
- 栈, 因为如果你不保存栈,那么局部变量你就无法恢复,同时函数的调用链你也无 法恢复,
- 寄存器的状态: 这好理解, 比如说EIP,如果你不保存,那么你恢复执行流就不知道 到底执行哪一条指令, 在比如说ESP,EBP, 如果你不保存,那么你即便有完整的栈 你也不知道怎么用.
这二者实际就是所谓的上下文,也可以说是continuation. 在执行流切换时必须保存 这两个东西, 内核调度进程时也是一回事.
- 不同点:
- 执行流的调度者不同, 进程是内核调度, 而协程是在用户态调度, 也就是说进程 的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的. 很显然用户态的 代价更低
- 进程会被抢占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程是不 可能得到执行机会,这实际和早期的操作系统类似,比如DOS, 它有一个yield原语, 一个进程调用yield,那么它就会让出CPU, 其他的进程也就有机会执行了, 如果一 个进程进入了死循环,那么整个系统也就挂起了,永远无法运行其他的进程了, 但 对协程而言,这不是问题
- 对内存的占用不同,实际上协程可以只需要4K的栈就够了, 而进程占用的内存要大 的多.
- 从操作系统的角度讲, 多协程的程序是单线程,单进程的
- 相同点:二者都是可以看做是一种执行流, 该执行流可以挂起,并且在将来又可以在 你挂起的地方恢复执行, 这实际上都可以看做是continuation, 我们来看看当我们挂 起一个执行流时我们要保存的东西
《Fluent Python》中对协程有这样的解释:
通过客户调用 .send(…) 方法发送数据或使用 yield from 结构驱动的生成器函数。
从生成器演变成协程
- 普通生成器generator通过调用
.send(value)
方法发送数据,且该数据将作为yield
表达式的值,所有生成器能够作为协程使用。 - 还添加了
.throw()
,用于抛出调用方异常,在生成器中处理;.close()
,用于终止生成器。 yield from
用于嵌套生成器。
协程的基本操作
协程的基本例子。
def simple_coroutine(): c = 0 print('coroutine start') x = yield c print('get --> %s' % x) c += 1 yield c sc = simple_coroutine() try: sc.send(1) except TypeError as e: print(e) # can't send non-None value to a just-started generator # coroutime start print(next(sc)) # 0 # get --> test print(sc.send('test')) # 1 try: next(sc) except StopIteration: print('coroutine end')
- 若创建协程后没有预激协程
next()
或send(None)
,而是send()
了一个非None的对象,则会抛出异常,当协程还未启动时候,是不能调用send()
向其发送一个非None的数据。 - 首次调用
next()
后,协程在yield
后暂停,且产出yield
后的值c
,若无c
,实际产出的是None
。 - 当协程暂停时,此时我们调用
send('test')
,向协程发送数据test
,此时协程恢复,yield
表达式计算并得到数据test
并赋值给x
,协程恢复执行直至下一个yield
处暂停,并产出该yield
后的c
的值,即1
。 - 最后恢复执行协程,此时已经到了协程末尾,抛出
StopIteration
异常。 - 协程的四种状态。GEN_CREATED等待开始执行、GEN_RUNNING正在执行、GEN_SUSPENDED在yield表达式处暂停、GEN_CLOSED结束。
# 四种状态 def simple_coroutine_for_state(): print('start') x = yield 1 print(inspect.getgeneratorstate(sc)) # GEN_RUNNING print('get -->%s' % x) sc = simple_coroutine_for_state() print(inspect.getgeneratorstate(sc)) # GEN_CREATED next(sc) print(inspect.getgeneratorstate(sc)) # GEN_SUSPENDED try: sc.send('test state') except StopIteration: print(inspect.getgeneratorstate(sc)) # GEN_CLOSED
- 若创建协程后没有预激协程
预激协程的装饰器:
- 如上所述,对于一个协程,
send()
方法的参数会成为yield
表达式的值,仅当协程处于暂停状态时,才能调用send()
非None方法,所以当协程处于未激活状态时,必须调用next()
或send()
方法来激活协程。 对于预激,《Fluent Python》中有如下描述:
最先调用 next(my_coro) 函数这一步通常称为“预激”(prime)协程
(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协
程使用)。简单的预激装饰器:
def prime_decorator(func): @wraps(func) def wrapper(*args, **kwargs): g = func(*args, **kwargs) next(g) return g return wrapper @prime_decorator def cal_average(): total = 0 count = 0 average = None while True: get = yield average total += get count += 1 average = total / count cal_gen = cal_average() print(cal_gen.send(100)) # 100.0 print(cal_gen.send(500)) # 300.0
Tornado中的
gen.coroutine
也是与预激协程有关:def coroutine(func, replace_callback=True): return _make_coroutine_wrapper(func, replace_callback=True) def _make_coroutine_wrapper(func, replace_callback): ······ @functools.wraps(wrapped) def wrapper(*args, **kwargs): future = TracebackFuture() ······ if isinstance(result, GeneratorType): try: orig_stack_contexts = stack_context._state.contexts yielded = next(result) ······ ······ return future ······ return wrapper
在预激装饰器内,除了简单的预激协程,也还能做一些其他更多的事情。
- 如上所述,对于一个协程,
终止协程和异常处理
- 终止协程:利用
generator.close()
方法来关闭协程,关闭后协程的状态变为GEN_CLOSED
。 - 异常处理:利用
generator.throw()
使协程在暂停的yield表达式处抛出指定异常,若该异常被处理,则协程向下执行且返回值是yield
的产出值;若该异常未被处理,则向上冒泡,传到调用方的上下文中,此时协程也会停止。
class DemoException(Exception): """""" def handle_exc_gen(): print('start') yielded_value = 0 try: while True: try: x = yield yielded_value except DemoException: print('handle demo exception and continuing') yielded_value = 0 else: print('receive %s' % x) yielded_value += 1 finally: print('end') exc_coro = handle_exc_gen() print(next(exc_coro)) # 预激协程 start 0 print(exc_coro.send('test')) # receive test 1 print(exc_coro.send('throw')) # receive throw 2 res = exc_coro.throw(DemoException) # handle demo exception and continuing print(res) # 0,处理DemoException时 yieled_value重新变为0 # 抛出未处理异常 exc_coro.throw(TypeError)
- 终止协程:利用
让协程返回值:在Python3.3之后可以在协程中写
return
语句而不会产生语法错误。此时协程返回数据的流程是:协程运行结束终止->抛出StopIteration
异常,异常对象的value
属性保存着协程的返回值->补货StopIteration
并获得返回值。def get_average(): count = 0 total = 0.0 average = None while True: get = yield average if get is None: break total += get count += 1 average = total / count return total, count get_average_coro = get_average() next(get_average_coro) get_average_coro.send(10) get_average_coro.send(30) try: get_average_coro.send(None) except StopIteration as e: print(e) # (40.0, 2)
依旧符合生成器对象的行为:耗尽后抛出
StopIteration
异常。
认识yield from
简单使用
yield from
:简化for循环中的yield
# 简化for循环中的yield def for_gen(): for s in 'ABC': yield s for i in range(1, 3): yield i for_g = for_gen() print(list(for_g)) # ['A', 'B', 'C', 1, 2] def simplify_for_gen(): yield from 'ABC' yield from range(1, 3) simplify_for_g = simplify_for_gen() print(list(simplify_for_g)) # ['A', 'B', 'C', 1, 2]
连接可迭代对象
def chain(*iterables): for it in iterables: yield from it s = 'ABC' n = range(1, 3) print(list(chain(s, n))) # ['A', 'B', 'C', 1, 2] def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, collections.Iterable) and not isinstance(x, ignore_types): yield from flatten(x) else: yield x items = ['a', ['b1', 'b2'], [['c11', 'c12'], ['c21', 'c22']]] print(list(flatten(items))) # ['a', 'b1', 'b2', 'c11', 'c12', 'c21', 'c22']
- 其他
小结
…