淺談 Gevent 與 Tornado(轉)


原文:http://www.pywave.com/2012/08/17/about-gevent-and-tornado/

還是前幾月的時候,幾乎在同一時間,自己接觸到了 Gevent 和 Tornado 這兩個已經不新的東西,那時那個 思緒混亂啊!似乎都支持異步,似乎都是無阻塞(non-blocking),性能似乎都好到個不行 (猛擊)。知道兩者雖是單線程, 但基於無阻塞的特性,戰斗力那個是嗖嗖地上漲,運用得當的話,hold住上K個連接不是問題。雖然很感興趣,雖然完全沒弄清楚兩者內里的實質,但為了完成工作,略略了解了基本的應用后,卷起手袖就上啦。 當然,作為一個有志的程序員,在滿足了現實的迫切需要后,一顆渴望知其所以然的心便開始蠢蠢欲動。

從 Tornado 說起

剛開始,對 Tornado 的感覺最為新鮮,在官網介紹里其是一個無阻塞的Web服務器以及相關工具的集合,但 個人更為傾向其為一個頗為完備的微型 web 框架。Tornado 性能好的關鍵是其無阻塞異步的特性,但這魔術 似的效果是如何達成的呢?迷思與困惑。我那小腦袋里的思維還停留於多進程(多線程)那樣的並發模型中, 實在有點難以理解 Tornado 的異步機制。

通過查閱各式文章以及源代碼,整體的框架脈絡開始逐漸在腦海中顯現出來。其實,Tornado 的異步模型 是由事件驅動以及特定的回調函數(callback)所組成的!一直沒有弄明白,Tornado 具體是如何實現 無阻塞異步,當清楚了事件驅動和回調函數的概念后,事情似乎又變得簡單起來了。

對於一般的程序,在執行階段若遇到 I/O 事件,整個進程將被阻塞住,直到 I/O 事件結束,程序又繼續執行。 接設我們對一些 I/O 事件進行了定制,使其可以立即返回(即無阻塞),那么程序將能立即繼續執行。但 問題又來了,那當 I/O 事件完成后又該怎么辦呢?此時,回調函數的威力就出來了,我只需要將進行特定 處理的回調函數與該 I/O 事件綁定起來,當該 I/O 事件完成后就調用綁定的回調函數,就可以處理具體的 I/O 事件啦。啊,似乎還有一個問題,回調函數要如何與 I/O 事件綁定起來?最簡單的想法是,直接通過 一個 while True 循環不斷的輪詢,當檢測到 I/O 事件完成了即觸發回調函數。但是,這樣的效率當然不會 高,利用系統中高效的 I/O 事件輪詢機制(epoll on Linux, kqueue on most BSD)就是最明智的 解決方案。於是,無阻塞 I/O +事件驅動+高效輪詢方式便組成了 Tornado 的異步模型。

Tornado 的核心是 ioloop 和 iostream 這兩個模塊,前者提供了一個高效的 I/O 事件循環,后者則封裝了 一個無阻塞的 socket 。通過向 ioloop 中添加網絡 I/O 事件,利用無阻塞的 socket ,再搭配相應的回調 函數,便可達到夢寐以求的高效異步執行啦。多說無益,來看一下具體的示例:

from tornado import ioloop
from tornado.httpclient import AsyncHTTPClient

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def print_head(response):
    print ('%s: %s bytes: %r' % (response.request.url,
                                 len(response.body),
                                 response.body[:50]))

http_client = AsyncHTTPClient()
for url in urls:
    print ('Starting %s' % url)
    http_client.fetch(url, print_head)
ioloop.IOLoop.instance().start()

因為使用了 AsyncHTTPClient 來處理請求操作,整個示例是異步執行的,即三個url請求無等待的依次發出。 我們可以看到 fetch 方法使用了 print_head 函數來作為回調函數,這意味着,當 fetch 完成了請求操作, 相應的 print_head 函數便會被觸發調用。恩,... 額,...,乍看起來,使用 Tornado 進行異步編程似乎 並不難,讓人躍躍欲試。但實際上,在現實生活中,事件驅動的編程還是會很費腦力,需要一定的創造性思維。 不過,這也許是 Tornado 受歡迎的原因之一呢。 :)

再來看下 Gevent

Gevent 是基於協程(coroutine)實現的 Python 網絡庫,使用了輕量級的 greenlet 作為執行單元,並 基於 libevent 事件循環構建了直觀的調用接口。

當時看到這樣的描述,腦袋的第一反應是,協程??稍稍了解后,發現協程其實也不是什么高深的概念,協程 也被稱為微線程,一看這別名就知道跟線程應該很類似。作為類比倒也可以這么認為,兩者關鍵的區別在於, 線程是由系統進行調度的,而協程是由用戶自己進行調度的。當知道這一事實后,立刻想到,這自行調度靈活 肯定是會很靈活,但要調度的話可是很有難度的吧?調度的方法暫時不談,除了更為靈活外,自行調度的直接 結果當然就是省去了系統調度(什么用戶態轉內核態,以及什么 context switch),因此協程間切換的資源 消耗很小,再配合協程生成成本很低的另一特點,這可真是相當的美妙。事實上,Python 語言本身就支持基礎 的協程的概念,generator 是其中的產物(這里)。

對於 Gevent,其使用的協程實際上就是 greenlet 。當你使用 greenlet 生成了一些協程,就可以在這些 協程里不斷跳轉執行,兩個 greenlet 之間的跳轉被稱為切換(switch)。通過切換,我們就可以實現對協程 的調度。還應該知道的是,每個 greenlet 都擁有一個父 greenlet ,這是在 greenlet 初始化時就確定的。 當一個 greenlet 執行完畢后,執行權會切換到其父 greenlet 中。實際上,所有的 greenlet 會被組織成 一顆樹,樹根便是最“老資格”的 greenlet ,這個老 greenlet 確定了各 greenlet 間的邏輯關系。

上面說到協程必須自行調度,不會是要自己構造一個調度器吧?這當然可以做到,但不是必須,因為 Gevent 已經基於 greenlet 和 libevent 封裝了許多基礎常用的庫,例如 socket 、event 和 queue 等,只要使用 這些庫進行開發,或者對使用的標准庫或第三方庫打一下補丁(monket patch),就能保證生成的各協程在 I/O 等待時正確地進行切換,從而實現無阻塞的異步執行。

剛接觸 Gevent 時,感覺跟傳統的並發編程很類似,但了解漸深后,才發現這貨實際上跟 Tornado 更為類似。 因為, Gevent 本質上也是事件驅動。實現的策略可以是,在將要執行 I/O 阻塞事件時,先在事件循環中對該事件 進行注冊,關聯的回調函數便是對當前協程的切換操作(current_greenlet.switch()),注冊成功后即 切換回當前協程的父協程中進行執行(current_greenlet.parent.switch())。當注冊的 I/O 事件被 觸發后,事件循環在恰當時機便會執行該回調函數,也就是切換到原先的協程繼續執行程序。從而,就實現 無阻塞的 I/O 事件處理。怎樣,是否感覺相當的有趣? :)

Gevent 了不得的地方還在於,我們能像編寫一般程序那樣來編寫異步程序,這可是彌足珍貴。為了更直觀的 顯示,讓我們來看一下具體的運行示例:

import gevent
from gevent import monkey
# patches stdlib (including socket and ssl modules) to cooperate with other greenlets
monkey.patch_all()

import urllib2

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def print_head(url):
    print ('Starting %s' % url)
    data = urllib2.urlopen(url).read()
    print ('%s: %s bytes: %r' % (url, len(data), data[:50]))

jobs = [gevent.spawn(print_head, url) for url in urls]

gevent.joinall(jobs)

 

上面示例做的事情實際上跟前面 Tornado 的示例是一樣,同樣是異步的對url進行請求。在我看來,使用 Gevent 進行編程,無論是可讀性還是可操作性都能讓人滿意。但也要清楚,在實際操作中,為了達到較理想 效果,經常還是需要根據不同的情況對代碼進行一些相應的“雕琢”。還有一點很常被人忽略, Gevent 是 基於協程實現的 Python 網絡庫,其適用面更多的是在於網絡 I/O 頻繁的需求里,很多情況下 Gevent 可能 並不是很好的選擇。總的來說,Gevent 確實很討人喜愛,性能好,開銷小,代碼易維護,是廣大 pythoner 手中的一大利器。

總要總結一下

作為一名 Python 程序員,在探究和使用 Tornado 與 Gevent 的過程里,除了得到許多思考的樂趣外,最 讓人高興的是收獲了一些全新的視野。使用 Python 編程的好處之一便是,可以很容易地跳出語言的框框去看 各式問題,從而提高自己對於程序設計的總體認識。人生苦短,我用Python! :-)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM