天天看點

gevent和tornado異步

閱讀目錄

  • 從 Tornado 說起
  • 再來看下 Gevent
  • 總要總結一下

原文: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 ,再搭配相應的回調 函數,便可達到夢寐以求的高效異步執行啦。多說無益,來看一下具體的示例:

gevent和tornado異步
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()      
gevent和tornado異步

因為使用了 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 了不得的地方還在于,我們能像編寫一般程式那樣來編寫異步程式,這可是彌足珍貴。為了更直覺的 顯示,讓我們來看一下具體的運作示例:

gevent和tornado異步
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)      
gevent和tornado異步

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

回到頂部

總要總結一下

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