简介

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

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

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

一个简单的协程切换例子

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

  1. def foo():
  2. print "running in foo"
  3. gevent.sleep(0)
  4. print "context switch to foo now"
  5. def bar():
  6. print "context switch to bar now"
  7. gevent.sleep(0)
  8. print "context switch to bar again"
  9. gevent.joinall([
  10. gevent.spawn(foo),
  11. gevent.spawn(bar)
  12. ])

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

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

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

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

图 1:代码执行顺序

14616862782384.gif

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

IO 堵塞切换

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

  1. import gevent
  2. import socket
  3. urls = ['www.google.com', 'www.example.com', 'www.python.org']
  4. def get_url_hostname(url):
  5. print "i will get hostname for url: {}".format(url)
  6. print socket.gethostbyname(url)
  7. print "i get hostname for url {} finish".format(url)
  8. jobs = [gevent.spawn(get_url_hostname, url) for url in urls]
  9. gevent.joinall(jobs)

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

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

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

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

  1. import gevent
  2. from gevent import socket
  3. urls = ['www.google.com', 'www.example.com', 'www.python.org']
  4. def get_url_hostname(url):
  5. print "i will get hostname for url: {}".format(url)
  6. print socket.gethostbyname(url)
  7. print "i get hostname for url {} finish".format(url)
  8. jobs = [gevent.spawn(get_url_hostname, url) for url in urls]
  9. gevent.joinall(jobs)

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

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

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

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

和 Flask 结合

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

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. from flask import Flask
  4. from gevent.pywsgi import WSGIServer
  5. app = Flask(__name__)
  6. app.debug = True
  7. if __name__ == "__main__":
  8. http = WSGIServer(('', 5050), app)
  9. http.serve_forever()

优缺点

优点

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

缺点

不优美的地方

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

参考资料