知乎自己在底層造了非常多的輪子,而且也在服務器部署方面和數據獲取方面廣泛使用 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