要了解协程,首先要对子例程有所认知。

子例程可以类比于程序中的函数调用,调用过程是后入先出的栈式调用;

协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。 协程-维基百科

通过下面的伪代码可以更好地理解协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var q := new queue
生产者协程
loop
while q is not full
create some new items
add the items to q
yield to consume
消费者协程
loop
while q is not empty
remove some items from q
use the items
yield to produce

在python中由于 全局解释器锁(GIL) 的原因导致多线程难以发挥多核并行计算能力,因为GIL的原因,同一时刻只能有一个线程运行,更多情况下我们选用multiprocessing来实现多进程从而提高执行效率。而对于协程则更擅长处理I/O密集的程序。

具体到python中协程,是以yield为基础实现的。

首先了解一下python中的yield:

可以通过stackoverflow上的一个帖子来了解python中的yield用法,简单地来说就是包含yield语句的函数被称为生成器函数,它与普通函数的区别就在于普通函数每次都要从函数入口处重新执行,而生成器函数仅第一次从入口处执行,之后都会从yield语句之后的地方开始执行。

这么看好像很复杂,下面我们通过一个例子来认识yield。

比如现在你想要写一个生成前n个斐波那契数,你可能很容易想到类似下面的这种写法:

1
2
3
4
5
6
7
8
9
10
def fib(n):
index = 0
a = 0
b = 1
res = []
while index < n:
index += 1
a, b = b, a+b
res.append(a)
return res

可是当n非常大时,上面这个程序把所有结果都保存在res中是很耗内存的,这时候就引出了yield。如果使用yield重写的话,函数是这样的:

1
2
3
4
5
6
7
8
9
10
11
def fib(n):
index = 0
a = 0
b = 1
while index < n:
index += 1
a, b = b, a+b
yield a
for i in fib(20):
print(i)

这里的fib因为包含了yield语句所以是生成器函数,当运行fib函数时,每次只取当前运算到的数,可以理解为每次运行到yield语句时,fib函数暂停,当下一次运行时,又会从暂停的位置继续运行。

那么讲到这里,这和协程有什么关系呢?

很容易想到如果能够从yield语句进入另一个函数并获取其返回值,不就是协程的实现吗?

这里引入了send的用法,send的作用即是把另一个函数的返回值传递给当前函数(PS:这里说函数返回值是为了帮助理解,其实就是再次进入函数时能够获取外界传递进来的参数),示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def stupid_fib(n):
index = 0
a = 0
b = 1
while index < n:
index += 1
a, b = b, a+b
#sleep_cnt 获取外界send的值
sleep_cnt = yield a
print('let me think {} secs'.format(sleep_cnt))
time.sleep(sleep_cnt)
sfib = stupid_fib(20)
fib_res = next(sfib)
while True:
print(fib_res)
try:
fib_res = sfib.send(random.uniform(0, 0.5))
except StopIteration:
break

目前为止,通过 yield 与 send 就实现了类似协程的机制。

要进一步了解协程可以深入地学习一下asyncio.coroutine和yield from以及python3.5中引入的新机制async和await。

另外本篇博文主要参考:Python协程:从yield/send到async/await