Gevent 性能和 gevent.loop 的运用和带来的思考

知乎自己在底层造了非常多的轮子,而且也在服务器部署方面和数据获取方面广泛使用 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

 

posted @ 2017-12-15 19:48  piperck  阅读(4382)  评论(0编辑  收藏  举报