简介

在gevent里面,上下文切换是通过yielding来完成的

当一个 greenlet 遇到 IO操作 时,比如访问网络,就自动切换到其他的 greenlet,等到 IO操作 完成,再在适当的时候切换回来继续执行。由于 IO操作 非常耗时,经常使程序处于等待状态,有了 gevent 为我们自动切换协程,就保证总有 greenlet 在运行,而不是等待IO。

由于切换是在 IO操作 时自动完成,所以 gevent 需要修改Python自带的一些标准库,这一过程在启动时通过 monkey patch 完成

一个简单的协程切换例子

前面说了,协程的切换时遇到 IO操作 时发生的,但是,这不是绝对的,也可以显式得调用 gevent.sleep 进行切换。下面就举一个例子来说明怎么显示切换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def foo():
    print "running in foo"
    gevent.sleep(0)
    print "context switch to foo now"

def bar():
    print "context switch to bar now"
    gevent.sleep(0)
    print "context switch to bar again"

gevent.joinall([
        gevent.spawn(foo),
        gevent.spawn(bar)
    ])

这里我们可以看到声明了两个函数,分别是 foobar,然后调用了 gevent.spawn 函数,最后再将他们 joinall 起来。

我们可以先来看一下输出:

1
2
3
4
running in foo
context switch to bar now
context switch to foo now
context switch to bar again

根据输出我们可以猜测代码的执行顺序应该是这样的:

14616862782384.gif

我们其实也就可以发现当代码执行到 gevent.sleep 的时候就发生了切换了。这就是一个简单的例子。

IO 堵塞切换

还是刚才的问题,我们一开始说了,协程是遇到 IO操作 耗时时会进行切换,那么按照这个道理,那么我们尝试写一段代码看看会不会:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import gevent
import socket

urls = ['www.google.com', 'www.example.com', 'www.python.org']

def get_url_hostname(url):
    print "i will get hostname for url: {}".format(url)
    print socket.gethostbyname(url)
    print "i get hostname for url {} finish".format(url)

jobs = [gevent.spawn(get_url_hostname, url) for url in urls]
gevent.joinall(jobs)

这里在 get_url_hostname 函数中做了一点点网络IO,用户获取域名的 ip,按道理,如果有网络IO阻塞就会切换协程的话,那么打印出来的顺序应该是杂乱的,我们运行一遍代码,看看输出是什么:

1
2
3
4
5
6
7
8
9
i will get hostname for url: www.google.com
216.58.199.4
i get hostname for url www.google.com finish
i will get hostname for url: www.example.com
93.184.216.34
i get hostname for url www.example.com finish
i will get hostname for url: www.python.org
103.245.222.223
i get hostname for url www.python.org finish

很神奇的是,我们运行一遍之后发现居然是顺序执行的,并没有切换协程,那么问题出在哪里呢?不是说好的切换的吗?

事实上,我们这里的 网络IO 使用的是 python 原生的,gevent 不能识别发生了阻塞,所以,这里 gevent 引入了猴子补丁(Monkey patching)的方式,让 gevent 知道网络发生了阻塞,我们这里对代码稍作修改,其实就修改一行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import gevent
from gevent import socket

urls = ['www.google.com', 'www.example.com', 'www.python.org']

def get_url_hostname(url):
    print "i will get hostname for url: {}".format(url)
    print socket.gethostbyname(url)
    print "i get hostname for url {} finish".format(url)

jobs = [gevent.spawn(get_url_hostname, url) for url in urls]
gevent.joinall(jobs)

如果你仔细看的话,会发现其实就更换了 import socket 这一行,我们不再使用 python 的原生 socket 了,而是使用 gevent 的猴子补丁,然后我们运行看看:

1
2
3
4
5
6
7
8
9
i will get hostname for url: www.google.com
i will get hostname for url: www.example.com
i will get hostname for url: www.python.org
216.58.203.4
i get hostname for url www.google.com finish
93.184.216.34
i get hostname for url www.example.com finish
103.245.222.223
i get hostname for url www.python.org finish

果然,这里如我们期待的那样,发生了协程的切换,大家都“同时”跑起来了。。

这就是 gevent 的简单运用,还有更多更复杂的功能需要继续发掘。留个坑。

和 Flask 结合

要想在 Flask 中使用 gevent 其实很简单,因为 Flask 对象就是一个 WSGI对象,所以直接引入 geventWSGIServer 即可,将 WSGI对象 传入提供 wsgi服务,简易代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/env python
# encoding: utf-8
from flask import Flask
from gevent.pywsgi import WSGIServer

app = Flask(__name__)
app.debug = True

if __name__ == "__main__":
    http = WSGIServer(('', 5050), app)
    http.serve_forever()

优缺点

优点

可以充分利用CPU资源,不会让进程阻塞,也不需要使用线程/进程。

缺点

不优美的地方

Gevent不能支持多进程。这是比其他问题更加蛋疼的部署问题, 这意味着如果你要完全用到多核,你需要在多个端口上运行多个监听进程。然后捏,你可能需要运行类似于Nginx的东西去在这些服务监听进程中分发请求(如果你服务需要处理HTTP请求的话)。但是我认为这不是大问题,因为在分布式应用中,本来就存在多个机器中的同个业务应用,所以即使跑多个进程也无关大雅。

参考资料