简介
- gevent: 一个基于 libev 的并发库,底层使用的是 epoll
- greenlet: gevent 中用到的主要模式,以 C 扩展模块形式接入 Python 的轻量型协程。全部运行在主程序操作系统进程的内部,但是被协作式地调度。
在gevent里面,上下文切换是通过yielding来完成的
当一个 greenlet 遇到 IO操作 时,比如访问网络,就自动切换到其他的 greenlet,等到 IO操作 完成,再在适当的时候切换回来继续执行。由于 IO操作 非常耗时,经常使程序处于等待状态,有了 gevent 为我们自动切换协程,就保证总有 greenlet 在运行,而不是等待IO。
由于切换是在 IO操作 时自动完成,所以 gevent 需要修改Python自带的一些标准库,这一过程在启动时通过 monkey patch 完成
一个简单的协程切换例子
前面说了,协程的切换时遇到 IO操作 时发生的,但是,这不是绝对的,也可以显式得调用 gevent.sleep 进行切换。下面就举一个例子来说明怎么显示切换:
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)
])
这里我们可以看到声明了两个函数,分别是 foo
和 bar
,然后调用了 gevent.spawn
函数,最后再将他们 joinall
起来。
我们可以先来看一下输出:
running in foo
context switch to bar now
context switch to foo now
context switch to bar again
根据输出我们可以猜测代码的执行顺序应该是这样的:
图 1:代码执行顺序 |
我们其实也就可以发现当代码执行到 gevent.sleep
的时候就发生了切换了。这就是一个简单的例子。
IO 堵塞切换
还是刚才的问题,我们一开始说了,协程是遇到 IO操作 耗时时会进行切换,那么按照这个道理,那么我们尝试写一段代码看看会不会:
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阻塞就会切换协程的话,那么打印出来的顺序应该是杂乱的,我们运行一遍代码,看看输出是什么:
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 知道网络发生了阻塞,我们这里对代码稍作修改,其实就修改一行:
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 的猴子补丁,然后我们运行看看:
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对象,所以直接引入 gevent 的 WSGIServer 即可,将 WSGI对象 传入提供 wsgi服务,简易代码如下:
#!/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 的文档太乱了,不是太有章法,比较难以入手。
兼容性
这里我特别想提到eventlet。回想起来,这是有一定道理的,它会导致一些匪夷所思的故障。我们用了一些 eventlet 在 MongoDB客户端代码上。当我使用 Gevent 的时候,它根本不能在服务器上运行。
使用顺序错误
在你导入Gevent或者说至少在你调用 Monkey.path_all() 之前启动监听进程。我不知道为什么,但这是我从邮件列表中学到的,另一点则是 Gevent 修改了Python 内部 Socket 的实现。当你启动一个监听进程,所有已经打开的文件描述符会被关闭,因此在子进程中,Socket 会以未修改过的形式重新创建出来,当然啦,这就会运行异常。Gevent需要处理这类的异常,或者说至少提供一个兼容的守护进程函数。
Monkey Pathing
当你执行monkey.path_all()的时候,很多操作会被打上补丁修改掉,这样可以使得普通 Python 模块能够很好的继续运行下去。奇怪的是,这丫的不是所有的东西都打上了这种补丁。我瞄了很久想去找出为毛的Signals模块不能运行,直到我发现是Gevent.signal 的问题。如果你想给函数打补丁,为毛的不全部打上咩?
不优美的地方
Gevent不能支持多进程。这是比其他问题更加蛋疼的部署问题, 这意味着如果你要完全用到多核,你需要在多个端口上运行多个监听进程。然后捏,你可能需要运行类似于Nginx的东西去在这些服务监听进程中分发请求(如果你服务需要处理HTTP请求的话)。但是我认为这不是大问题,因为在分布式应用中,本来就存在多个机器中的同个业务应用,所以即使跑多个进程也无关大雅。