知乎自己在底层造了非常多的轮子,而且也在服务器部署方面和数据获取方面广泛使用 gevent 来提高并发获取数据的能力。现在开始我将结合实际使用与测试慢慢完善自己对 gevent 更全面的使用和扫盲。
在对 gevent loop 的使用上,gevent tutorial 介绍得非常敷衍,以至于完全不知道他的使用办法。这里我将结合 timeit 测试更详细的介绍一下 gevnet.loop 的使用。以及他的父类 Group 的使用。
其实在使用 gevent 上面我个人一直有一个误区,就是我使用并发的 gevent 一定比我平时线性的操作速度更快,其实不是这样。让我们来看一个例子:
import timeit
import gevent
def task1(): pass def task(): for i in range(50): pass def async(): x = gevent.spawn(task) x.join() def sync(): for i in range(50): task1() print timeit.timeit(stmt=async, setup=''' from __main__ import task, async, sync ''', number=1000) print '同步开始了' print timeit.timeit(stmt=sync, setup=''' from __main__ import task, async, sync ''', number=1000)
output:
0.0216090679169
同步开始了
0.00430107116699
可以看到,我们同样跑一样的函数调用,如果使用 gevent.spawn 一个调用,我们会话费更多的资源,这导致了我们甚至没有线性完成得快。你可能会说,这是当然了,因为这里只 spwan 了一个 gevent 的 greenlet 实例。如果我们调用多个呢?
import timeit import gevent def async1(): p = [] for i in range(50): p.append(gevent.spawn(task1)) gevent.joinall(p) def task1(): pass def sync(): for i in range(50): task1() print timeit.timeit(stmt=async1, setup=''' from __main__ import task, async1, sync ''', number=1000) print '同步开始了' print timeit.timeit(stmt=sync, setup=''' from __main__ import task, async1, sync ''', number=1000) output: 1.21793103218 同步开始了 0.0048680305481
情况似乎变得更糟糕了。。。。我们同时 spawn 了 50个 greenlet 实例实图一次性搞定这个事情,但是速度甚至变得更慢了。由此我们可以得出一个结论,也许在并不是在网络请求或者需要等待切换的情况下,使用 gevent 也许不是一个很好的解决方案。
那到底种情况可以使我们的性能获得巨大的提升?来看这个例子:
import timeit import gevent def async1(): p = [] for i in range(50): p.append(gevent.spawn(task1)) gevent.joinall(p) def task1(): gevent.sleep(0.001) def sync(): for i in range(50): task1() print timeit.timeit(stmt=async1, setup=''' from __main__ import task1, async1, sync ''', number=100) print '同步开始了' print timeit.timeit(stmt=sync, setup=''' from __main__ import task1, async1, sync ''', number=100) output: 0.25629901886 同步开始了 6.91364789009
可以看出来,这次我 spawn 50个一起跑,就远远快于线性了。因为在线性的情况下,我们每次都会在 task1 任务运行的时候阻塞 0.001s, 但是 gevent 使得 async 函数几乎不受等待影响。非常快速的解决了这个问题。其实这个环境在我们进行网络 io 的时候非常常见。比如我们向某个地址下载图片,如果我们线性下载图片,我们需要等待第一张图片下载完成之后才能进行第二张图片的下载,但是我们使用 gevent 并发下载图片,我们可以先开始下载图片,然后在等待的时候切换到别的任务继续进行下载。当下载完毕之后我们会切换回来完成下载。不用等待任何一个任务下载完成,大大的提高了效率。
gevent 的 pool 函数可以控制并发的时候最多使用 greenlet 的数量。 这里我循环了50次,但是当我们在进行 io 的时候,我们设置了 1w 次,那么也会起 10000 个协程来运行这个程序,对于性能我们是不知道的。有可能会直接堵死服务器端,所以我们需要对此进行控制,我们限制最多同时使用 20 个 greenlet 实例进行处理,当有任务完成之后我们再开始别的任务,更好的控制我们的请求以及维护相当的效率让我们来看几个数据:
开 10个 greenlet 的情况
import timeit import gevent from gevent.pool import Pool x = Pool(40) def async1(): for i in range(50): x.spawn(task1) x.join() def task1(): gevent.sleep(0.001) def sync(): for i in range(50): task1() print timeit.timeit(stmt=async1, setup=''' from __main__ import task1, async1, sync ''', number=100) print '同步开始了' print timeit.timeit(stmt=sync, setup=''' from __main__ import task1, async1, sync ''', number=100) output:
0.813331842422
同步开始了
6.89506411552
开 40 个实例的情况:
0.366757154465
同步开始了
6.78097295761
开80 个实例的情况:
0.222685098648
同步开始了
6.77246403694
开10000个的情况:
0.227874994278
同步开始了
6.81039714813
可以看到当我们超过阀值之后,开更多的实例已经没有任何意义了。而且有可能还造成一些性能上的浪费,所以选择一个合适的实例数量即可。
另外还有一个速度更快的函数可以提供使用:
def async1(): for i in range(50): x.imap(task1)
官方文档上还有一句话,就是如果对出的结果并不要求顺序的话可以使用imap_unordered,速度更快:
def async1(): for i in range(50): x.imap_unordered(task1)
pool饱和的情况下 上面的例子差不多只要 0.8s 就能处理完,imap 需要1s。使用join需要 0.22s。
Reference:
http://hhkbp2.github.io/gevent-tutorial/#_8 gevent-tutorial