生成器generator
在讨论协程之前,我们先来看看python的生成器
。简单的来讲,在python里面,一边循环一边计算的机制叫做生成器
。举个例子。
生成一个有1000个元素的列表可以这样生成:
l = [i for i in range(1000)]
print l
# [0, 1, 2, 3, 4, ...]
这种方法,在内存生成了一个有1000个元素的列表。比较占用内存。
同样生成1000个元素的列表,这次采用生成器的方式:
l = (i for i in range(1000))
print l
# <generator object <genexpr> at 0x0000000003334410>
只把[]
换成了()
,列表就变成了生成器。两者得到的有什么区别?
# 使用第一种方法生成的列表,因为列表已经存在内存里了,所以可以多次迭代,得到一样的结果
for time in loop:
for i in l:
print i
# 1,2,3,4,......,999,1,2,3,4,......,999
# 而使用第二种方法生成的,只能迭代一次,迭代过程中,一边计算下一个结果,一边返回输出,如果进行第二次迭代将得到空结果
for time in loop:
for i in l:
print i
# 1,2,3,4,......,999
在python2中,range和xrange就是这样的区别,这也是xrange比range性能要好的原因
关键字yield
yield在一般场景很难用上,使用yield的效果就是得到生成器。我们用yield编写一个函数实现上面生成器的效果:
def n1k():
i = 0
while i < 1000:
yield i
i += 1
###################################################
# 调用函数,得到生成器
gen = n1k()
print gen
# <generator object n1k at 0x00000000033344C0>
# 调用生成器
for i in gen:
print i
# 1,2,3,4,......,999
yield将函数变成了一个生成器,python解析器在调用这个函数的时候,不会去执行这个函数,而是得到一个迭代器去迭代这个生成器。
所以一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
协程
从python yield的角度去理解协程比较难理解,我们先来看看协程的概念。
在高并发的场景下,由于多线程切换的开销比较大,于是有了协程,协程实质是在用户态实现了的线程,在系统内核态,是不能感知协程的存在,协程的切换由用户程序自己控制。
要注意协程的特点,在用户态执行中断,并且切换上下文,来在同一个空间中完成不同的处理任务。
这里的中断怎么理解,在迭代器里面,我们可以调用next
函数随时的迭代出下一个元素。
l = (i for i in range(1000))
l.next()
# 0
l.next()
# 1
迭代器可以调用next
迭代出下一个元素的方法,就是一种中断,在得到一个迭代器之后,用户程序可以随时随地的在任意逻辑任意时间迭代和停止迭代,这就是中断。而在停止中断以后,程序运行了别的逻辑,这就相当于切换了上下文。
所以,yield契合了协程的特性,因而才有了python用yield实现协程的内容。
理解了协程之后,我们来看看一个生产者消费者的例子(来自廖雪峰官方网站的协程例子):
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待。
改用协程:
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) # 通知迭代器返回下一个
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__=='__main__':
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:
- 首先调用c.next()启动生成器;
- 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
- consumer通过yield拿到消息,处理,又通过yield把结果传回;
- produce拿到consumer处理的结果,继续生产下一条消息;
- produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
所以,通过使用yield生成的迭代器,是实现协程的一种方式。
而在go语言里面,goroutine
是协程的另外一种方式。
所以使用yield实现协程比较难理解。如果将goroutine
比喻成已经成品的车,那么yield就相当于提供了车的零件,还需要自己去拼装实现。