python协程

协程的主要特点是 允许执行被挂起和被恢复,当然线程也是可以被挂起和恢复的,那么线程和协程在被挂起和恢复之间最大的区别是:线程的挂起和恢复需要陷入到内核,而协程的不用;

本质上讲协程是函数,函数在执行的过程中,由语言的运行时环境(比如go runtime)或者专门用来实现协程的包(比如gevent)来实现对任务的挂起和恢复的。

使用一个生产者-消费者的例子来说明协程:

1
var q  := new queue

生产者:

1
2
3
4
5
loop
while q is not full
create some new items
add the items to q
yield to consume

消费者协程:

1
2
3
4
5
loop
while q is not empty
remove some items from q
use the items
yield to produce

每个协程在用yield命令向另一个协程交出控制时都尽可能做了更多的工作。放弃控制使得另一个例程从这个例程停止的地方开始,但因为现在队列被修改了所以他可以做更多事情.

Python 生成器(yield 关键字)

1
2
3
4
5
6
7
def count(n):
x = 0
while x < n:
yield x
x += 1
for i in count(5):
print i

上述代码的作用是打印0到4,但请注意,其中多了一个关键字yield

1
2
3
gen = [x for x in range(5)
for i in gen:
print i

这段代码也是打印了0到4,那么和带有yield关键字的有什么区别呢?

区别在于,第一个段代码定义的count其实是个生成器,而第二段代码定义的gen只是一个静态的tuple;

而生成器最大的使用方式就是

1
2
3
4
gen = count(2)
print(gen.next()) # 打印0
print(gen.next()) # 打印1
print(gen.next()) # 抛出异常StopIteration

看到没有,生成器在遇到yield关键字的时候,会返回其后的值,并且具有记忆功能,等到下次调用next的时候,会继续返回接下来的值。

有一种场景特别适合使用生成器,比如,生成一个程度为10000的斐波那契序列,如果使用迭代器,那么需要1000个存储空间,而如果使用生成器,那么就不必为这10000个数分配存储空间

1
2
3
4
5
6
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

greenlet

greenlet实现的功能和yield类似,只不过做了更好的封装,下面是其官方文档的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from greenlet import greenlet

def test1():
print 12
gr2.switch() # 切换协程,交出cpu使用权
print 34

def test2():
print 56
gr1.switch() # 切换协程,交出cpu使用权
print 78

gr1 = greenlet(test1) # 生成一个greenlet对象
gr2 = greenlet(test2) # 生成一个greenlet对象
gr1.switch() #交出cpu使用权

上述代码的输出是:

1
2
3
12
56
34

程序在第6行结束。

可以看到,相对yield关键字,greenlet提供了一种更简单直白的使用方式:显式的让出cpu的使用权,而且让出CPU使用权的时候,还可以传递数据。

关于greenlet大概只需要了解这么多,因为它封装的接口很简单。

gevent

gevent是基于greenlet封装的功能比较完善的协程库,greenlet提供了从一个协程切换到另一个协程的方式:通过显式的调用switch()来实现,然而在实际生产中,如果都显式的调用switch()那么代码就会混乱不堪,gevent的主要功能就是提供了自动切换协程的能力:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from gevent import monkey;
monkey.patch_all()
import gevent
import requests

def f(url):
print('GET: %s' % url)
resp = requests.get(url)
data = resp.text
print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
gevent.spawn(f, 'https://www.baidu.com/'),
gevent.spawn(f, 'https://www.google.com/'),
gevent.spawn(f, 'https://www.python.org/'),
])

运行结果:

1
2
3
4
5
GET: https://www.baidu.com/
GET: https://www.google.com/
GET: https://www.python.org/
2443 bytes received from https://www.baidu.com/.
14021 bytes received from https://www.google.com/.

我们注意到,在代码中有一行

1
monkey.patch_all()

这是因为,Python的运行环境允许我们在运行时修改大部分的对象,包括模块,类甚至函数。 这是个一般说来令人惊奇的坏主意,因为它创造了“隐式的副作用”,如果出现问题 它很多时候是极难调试的。

虽然如此,在极端情况下当一个库需要修改Python本身 的基础行为的时候,猴子补丁就派上用场了。在这种情况下,gevent能够 修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。

例如,Redis的python绑定一般使用常规的tcp socket来与redis-server实例通信。 通过简单地调用gevent.monkey.patch_all(),可以使得redis的绑定协作式的调度 请求,与gevent栈的其它部分一起工作。

这让我们可以将一般不能与gevent共同工作的库结合起来,而不用写哪怕一行代码。 虽然猴子补丁仍然是邪恶的(evil),但在这种情况下它是“有用的邪恶(useful evil)”。

参考文献

  • https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B
  • http://www.bjhee.com/python-yield.html
  • http://www.bjhee.com/greenlet.html
  • https://www.jianshu.com/p/d8ef044e8c7c
  • http://hhkbp2.com/gevent-tutorial/