Python协程知多少

开发 后端
从概念上来说,我们都知道多进程和多线程,而协程其实是在单线程中实现多并发。从句法上看,协程与生成器类似,都是定义体中包含yield关键字的函数。区别在于协程的yield通常出现在表达式的右边:datum = yield。

 [[439431]]

从概念上来说,我们都知道多进程和多线程,而协程其实是在单线程中实现多并发。从句法上看,协程与生成器类似,都是定义体中包含yield关键字的函数。区别在于协程的yield通常出现在表达式的右边:datum = yield。这一下就让初学者瞬间觉得yield关键字不香了,本来以为yield就是简简单单的暂停执行顺手返回个值,结果还能放右边?

从生成器到协程

先看一个可能是协程最简单的使用示例:

  1. >>> def simple_coroutine(): 
  2. ...     print("-> coroutine started"
  3. ...     x = yield 
  4. ...     print("-> coroutine received:", x) 
  5. ...      
  6. >>> my_coro = simple_coroutine() 
  7. >>> my_coro 
  8. <generator object simple_coroutine at 0x0000019A681F27B0> 
  9. >>> next(my_coro) 
  10. -> coroutine started 
  11. >>> my_coro.send(42) 
  12. -> coroutine received: 42 
  13. Traceback (most recent call last): 
  14.   File "<input>", line 1, in <module> 
  15. StopIteration 

之所以yield可以放右边,是因为协程可以接收调用方使用.send()推送的值。

yield放在右边以后,它的右边还能再放个表达式,请看下面这个例子:

  1. def simple_coro2(a): 
  2.     b = yield a 
  3.     c = yield a + b 
  4.  
  5. my_coro2 = simple_coro2(14) 
  6. next(my_coro2) 
  7. my_coro2.send(28) 
  8. my_coro2.send(99) 

执行过程是:

  • 调用next(my_coro2),执行yield a,产出14。
  • 调用my_coro2.send(28),把28赋值给b,然后执行yield a + b,产出42。
  • 调用my_coro2.send(99),把99赋值给c,协程终止。

由此得出结论,对于b = yield a这行代码来说,= 右边的代码在赋值之前执行。

在示例中,需要先调用next(my_coro)启动生成器,让程序在yield语句处暂停,然后才可以发送数据。这是因为协程有四种状态:

  • 'GEN_CREATED' 等待开始执行
  • 'GEN_RUNNING' 解释器正在执行
  • 'GEN_SUSPENDED' 在yield表达式处暂停
  • 'GEN_CLOSED' 执行结束

只有在GEN_SUSPENDED状态才能发送数据,提前做的这一步叫做预激,既可以调用next(my_coro)预激,也可以调用my_coro.send(None)预激,效果一样。

预激协程

协程必须预激才能使用,也就是send前,先调用next,让协程处于GEN_SUSPENDED状态。但是这件事经常会忘记。为了避免忘记,可以定义一个预激装饰器,比如:

  1. from functools import wraps 
  2.  
  3. def coroutine(func): 
  4.     @wraps(func) 
  5.     def primer(*args, **kwargs): 
  6.         gen = func(*args, **kwargs) 
  7.         next(gen) 
  8.         return gen 
  9.     return primer 

但实际上Python给出了一个更优雅的方式,叫做yield from,它会自动预激协程。

自定义预激装饰器和yield from是不兼容的。

yield from

yield from相当于其他语言中的await关键字,作用是:在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。

yield from可以用来简化for循环中的yield:

  1. for c in "AB"
  2.     yield c 
  3. yield from "AB" 

yield from x表达式对x做的第一件事就是,调用iter(x),从中获取迭代器。

但yield from的作用远不止于此,它更重要的作用是打开双向通道。如下图所示:

这个图信息量很大,很难理解。

首先要理解这3个概念:调用方、委派生成器、子生成器。

  • 调用方

说白了就是main函数,也就是众所周知的程序入口main函数。

  1. # the client code, a.k.a. the caller 
  2. def main(data):  # <8> 
  3.     results = {} 
  4.     for keyvalues in data.items(): 
  5.         group = grouper(results, key)  # <9> 
  6.         next(group)  # <10> 
  7.         for value in values
  8.             group.send(value)  # <11> 
  9.         group.send(None)  # important! <12> 
  10.  
  11.     # print(results)  # uncomment to debug 
  12.     report(results) 
  • 委派生成器

就是包含了yield from语句的函数,也就是协程。

  1. # the delegating generator 
  2.  
  3. def grouper(results, key): # <5> 
  4.  
  5. while True: # <6> 
  6.  
  7. results[key] = yield from averager() # <7> 
  • 子生成器

就是yield from语句右边跟着的子协程。

  1. # the subgenerator 
  2. def averager():  # <1> 
  3.     total = 0.0 
  4.     count = 0 
  5.     average = None 
  6.     while True
  7.         term = yield  # <2> 
  8.         if term is None:  # <3> 
  9.             break 
  10.         total += term 
  11.         count += 1 
  12.         average = total/count 
  13.     return Result(count, average)  # <4> 

这比术语看着舒服多了。

然后是5条线:send、yield、throw、StopIteration、close。

  • send

协程在yield from表达式处暂停时,main函数可以通过yield from表达式把数据发给yield from语句右边跟着的子协程。

  • yield

yield from语句右边跟着的子协程再把产出的值通过yield from表达式发给main函数。

  • throw

main函数通过group.send(None),传入一个None值,让yield from语句右边跟着的子协程的while循环终止,这样控制权才会交回协程,才能继续执行,否则会一直暂在yield from语句暂停。

  • StopIteration

yield from语句右边跟着的生成器函数返回之后,解释器会抛出StopIteration异常。并把返回值附加到异常对象上,此时协程会恢复。

  • close

main函数执行完以后,会调用close()方法退出协程。

大体流程搞清楚了,更多的技术细节就不继续研究了,有时间的话,在以后的Python原理系列中再学习吧。

yield from经常与Python3.4标准库里的@asyncio.coroutine装饰器结合使用。

协程用作累加器

这是协程的常见用途,代码如下:

  1. def averager(): 
  2.     total = 0.0 
  3.     count = 0 
  4.     average = None 
  5.     while True:  # <1> 
  6.         term = yield average  # <2> 
  7.         total += term 
  8.         count += 1 
  9.         average = total/count 

协程实现并发

这里例子有点复杂,源码地址是:

https://github.com/fluentpython/example-code/blob/master/16-coroutine/taxi_sim.py

核心代码片段是:

  1. BEGIN TAXI_PROCESS 
  2. def taxi_process(ident, trips, start_time=0):  # <1> 
  3.     """Yield to simulator issuing event at each state change""" 
  4.     time = yield Event(start_time, ident, 'leave garage')  # <2> 
  5.     for i in range(trips):  # <3> 
  6.         time = yield Event(time, ident, 'pick up passenger')  # <4> 
  7.         time = yield Event(time, ident, 'drop off passenger')  # <5> 
  8.  
  9.     yield Event(time, ident, 'going home')  # <6> 
  10.     # end of taxi process # <7> 
  11. END TAXI_PROCESS 
  1. def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, 
  2.          seed=None): 
  3.     """Initialize random generator, build procs and run simulation""" 
  4.     if seed is not None: 
  5.         random.seed(seed)  # get reproducible results 
  6.  
  7.     taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL) 
  8.              for i in range(num_taxis)} 
  9.     sim = Simulator(taxis) 
  10.     sim.run(end_time) 

这个示例说明了如何在一个主循环中处理事件,以及如何通过发送数据驱动协程。这是asyncio包底层的基本思想。使用协程代替线程和回调,实现并发。

参考资料:

《流畅的Python》第16章 协程    https://zhuanlan.zhihu.com/p/104918655

 

责任编辑:武晓燕 来源: dongfanger
相关推荐

2023-12-24 12:56:36

协程

2021-09-16 09:59:13

PythonJavaScript代码

2024-02-05 09:06:25

Python协程Asyncio库

2012-02-13 22:50:59

集群高可用

2017-09-22 16:08:16

Python协程编程

2017-06-15 13:15:39

Python协程

2010-08-16 09:15:57

2021-12-04 11:17:32

Javascript继承编程

2013-12-23 14:00:31

Windows 8.2Windows 8.1

2023-11-17 11:36:59

协程纤程操作系统

2017-07-14 10:51:37

性能优化SQL性能分析

2017-09-06 09:26:03

Python生成器协程

2023-10-24 19:37:34

协程Java

2018-08-31 10:53:25

MySQL存储引擎

2020-09-08 10:56:55

Java多线程存储器

2021-07-22 07:20:24

JS 遍历方法前端

2012-09-10 16:38:40

Windows Ser

2009-05-13 17:31:06

DBAOracleIT

2020-04-07 11:10:30

Python数据线程

2020-12-17 10:00:16

Python协程线程
点赞
收藏

51CTO技术栈公众号