生成器与协程

生成器(generator):是一种⼀边循环⼀边计算的机制

是一种用普通函数语法定义的迭代器.生成器一定是迭代器(反之不成立)

创建生成器

第⼀种⽅法很简单,只要把⼀个列表⽣成式的[] 改成()

生成器推导式
1
2
3
4
d=(x*x for x in range(10))
sum(d)
# 285
d.next() # StopIteration sum(d)已经迭代完了
yield

yield 保持简洁性的同时获得了 iterable 的效果.把一个函数改写为一个 generator 就获得了迭代能力

yield 是一个类似 return 的关键字,只是这个函数返回的是个生成器

yield 进程挂起 保持连接

使用生成器,定义斐波那契数列

1
2
3
4
5
6
7
8
9
10
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b #
# yield 的作用就是把一个函数变成一个 generator
# 调用 fab(5)不会执行fab函数,而是返回一个iterable对象
a, b = b, a + b
n = n + 1
# 如果没有return,则默认执行至函数完毕,如果在执行过程中return
#则直接抛出StopIteration终止迭代

1
for n in fab(5): print n

在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时
fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行
而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield
看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值.

1
2
f = fab(5)
print(f.next())

手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),
这样我们就可以更清楚地看到 fab 的执行流程

1
2
f1 = fab(5)
print(f1.next())

每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响

多个yield

1
2
3
4
5
6
7
8
9
def test_yield():
yield 1
yield 2
yield (1,2)
g = test_yield()
print(next(g))
print(next(g))
# print(next(g))

send()

1
2
3
4
5
6
7
8
9
10
11
12
13
def gen():
i = 0
while i < 5:
temp = yield i
# temp 并不是i的值
print( temp )
i+=1
f = gen() # 不是调用了函数,而是返回了生成器 生成器的值就是返回值
print(next(f)) # 0
print(next(f)) # None 1
print(f.send('femn')) # femn 2
# c.next()等价c.send(None)

⽣成器是这样⼀个函数,它记住上⼀次返回时在函数体中的位置.对⽣成器 函数的第⼆次(或第n次)调⽤跳转⾄该函数中间,⽽上次调⽤的所有局部变量都保持不变.

⽣成器不仅”记住”了它数据状态;⽣成器还”记住”了它在流控制构造(在命令 式编程中,这种构造不只是数据值)中的位置.

⽣成器的特点:节约内存. 迭代到下⼀次的调⽤时,所使⽤的参数都是第⼀次所保留下的,即是说,在整个所有函数调⽤的参数都是第⼀次所调⽤时保留的,⽽不是新创建的

yield控制进程的暂停与send又恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import time
import threading
gen = None # 全局生成器,供long_io使用
def long_io():
def fun():
print("开始执行IO操作")
global gen
time.sleep(5)
try:
print("完成IO操作,并send结果唤醒挂起程序继续执行")
gen.send("io result") # 使用send返回结果并唤醒程序继续执行
except StopIteration: # 捕获生成器完成迭代,防止程序退出
pass
threading._start_new_thread(fun, ())
def req_a():
print("开始处理请求req_a")
ret = yield long_io()
print("ret: %s" % ret)
print("完成处理请求req_a")
def req_b():
print("开始处理请求req_b")
time.sleep(2)
print( "完成处理请求req_b")
def main():
global gen
gen = req_a() # 得到生成器,不能像同步一样去输写
next(gen) # 开启生成器req_a的执行
req_b()
while 1:
pass
if __name__ == '__main__':
main()

yiled多任务(协程,进程,线程)

协程:单进程单线程的多任务执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test1():
while True:
print("---1---")
yield None
def test2():
while True:
print("---2---")
yield None
t1 = test1()
t2 = test2()
while True:
next(t1)
next(t2)
# 暂停和启动之间的快速切换

yield ⼈⼯自动切换
而协程是由程序员自己决定怎样切换执行的,并且只能有一个执行路径

计算密集型需要占用大量的CPU资源,使用多进程
IO密集型需要网络功能,大量的时间都在等待网络数据的到来,使用多线程或线程
只切换了函数的调用,只要求保存函数的变量,而不像进程和线程的切换时,需要的其它内容的上下文
进程和线程的调用是由操作系统决定切换而执行的

gevent自动切换执行

1
2
3
4
5
6
7
8
9
10
11
12
import gevent
deff(n):
fori in range(n):
print(gevent.getcurrent(),i)
#用来模拟一个耗时操作,注意不是time模块中的sleep
gevent.sleep(1)
g1=gevent.spawn(f,5)
g2=gevent.spawn(f,5)
g3=gevent.spawn(f,5)
g1.join()
g2.join()
g3.join()

当⼀个greenlet遇到IO(指的是input output 输⼊输出,⽐如⽹络、⽂件操作等)操作时,⽐如访问⽹络,就⾃动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执⾏
由于IO操作⾮常耗时,经常使程序处于等待状态,有了gevent为我们⾃动切换协程,就保证总有greenlet在运⾏,⽽不是等待IO

协程(又称微线程(coroutine),相当于单线程的能力)与子程序(函数调用是通过栈实现的,一个线程就是执行一个子程序,子程序就是协程的一种特例)

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕.
协程(协作完成任务),tornado好像就是用协程来实现异步的协程,协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行.

协程的特点在于是一个线程执行,所有协程有如下优势

最大的优势就是协程极高的执行效率.因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显.

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,
# 通过锁机制控制队列和等待,但一不小心就可能死锁.
# 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行
# 待消费者执行完毕后,切换回生产者继续生产,效率极高
import time
def consumer():
r = ''
while True:
n = yield r
# 3.consumer通过yield拿到消息,处理完又通过yield把结果传回
if not n: return print('[CONSUMER] Consuming %s...' % n)
print(n)
time.sleep(1)
r = '200 OK'
def produce(c):
next(c) # 不是值 1. 启用生成器
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
# 这里才是迭代器中的值 2. 一旦生产了东西,通过c.send(n)切换到consumer执行
print('[PRODUCER] Consumer return: %s' % r)
# 4. produce拿到consumer处理的结果,继续生产下一条消息
c.close()
# 5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束
if __name__ == '__main__':
c = consumer()
produce(c)

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为”协程”,而非线程的抢占式多任务. 子程序就是协程的一种特例.
线程确实比协程性能更好.因为线程能利用多核达到真正的并行计算,如果任务设计的好,线程能几乎成倍的提高你的计算能力,说线程性能不好的很多 是因为没有设计好导致大量的锁,切换,等待,这些很多都是应用层的问题.
而协程因为是非抢占式,所以需要用户自己释放使用权来切换到其它协程,因此 同一时间其实只有一个协程拥有运行权,相当于单线程的能力

Share Comments