學習tornado:非同步
why asynchronous
tornado是一個非同步web framework,說是非同步,是因為tornado server與client的網路互動是非同步的,底層基於io event loop。但是如果client請求server處理的handler裡面有一個阻塞的耗時操作,那麼整體的server效能就會下降。
def MainHandler(tornado.web.RequestHandler): def get(self): client = tornado.httpclient.HttpClient() response = client.fetch("http://www.google.com/") self.write('Hello World')
在上面的例子中,tornado server的整體效能依賴於訪問google的時間,如果訪問google的時間比較長,就會導致整體server的阻塞。所以,為了提升整體的server效能,我們需要一套機制,使得handler處理都能夠通過非同步的方式實現。
幸運的是,tornado提供了一套非同步機制,方便我們實現自己的非同步操作。當handler處理需要進行其餘的網路操作的時候,tornado提供了一個async http client用來支援非同步。
def MainHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): client = tornado.httpclient.AsyncHTTPClient() def callback(response): self.write("Hello World") self.finish() client.fetch("http://www.google.com/", callback)
上面的例子,主要有幾個變化:
- 使用asynchronous decorator,它主要設定_auto_finish為false,這樣handler的get函式返回的時候tornado就不會關閉與client的連線。
- 使用AsyncHttpClient,fetch的時候提供callback函式,這樣當fetch http請求完成的時候才會去呼叫callback,而不會阻塞。
- callback呼叫完成之後通過finish結束與client的連線。
asynchronous flaw
非同步操作是一個很強大的操作,但是它也有一些缺陷。最主要的問題就是在於callback導致了程式碼邏輯的拆分。對於程式設計師來說,同步順序的想法是一個很自然的習慣,但是非同步打破了這種順序性,導致程式碼編寫的困難。這點,對於寫nodejs的童鞋來說,可能深有體會,如果所有的操作都是非同步,那麼最終我們的程式碼可能寫成這樣:
def MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
client = tornado.httpclient.AsyncHTTPClient()
def callback1(response):
def callback2(response):
self.write("Hello World")
self.finish()
client.fetch("http://www.google.com", callback2)
client.fetch("http://www.google.com/", callback1)
也就是說,我們可能會寫出callback巢狀callback的情況,這個極大的會影響程式碼的閱讀與流程的實現。
synchronous
我個人認為,非同步拆散了程式碼流程這個問題不大,畢竟如果一個邏輯需要過多的巢狀callback來實現的話,那麼我們就需要考慮這個邏輯是否合理了,所以非同步一般也不會有過多的巢狀層次。
雖然我認為非同步的callback問題不大,但是如果仍然能夠有一套機制,使得非同步能夠順序化,那麼對於程式碼邏輯的編寫來說,會方便很多。tornado有一些機制來實現。
yield
在python裡面如果一個函式內部實現了yield,那麼這個函式就不是函數了,而是一個生成器,它的整個執行機制也跟普通函式不一樣,舉一個例子:
def test_yield():
print 'yield 1'
a = yield 'yielded'
print 'over', a
t = test_yield()
print 'main', type(t)
ret = t.send(None)
print ret
try:
t.send('hello yield')
except StopIteration:
print 'yield over'
輸出結果如下:
main <type 'generator'>
yield 1
yielded
over hello yield
yield over
從上面可以看到,test_yield是一個生成器,當它第一次呼叫的時候,只是生成了一個Generator,不會執行。當第一次呼叫send的時候,生成器被resume,開始執行,然後碰到yield,就掛起,等待下一次被send喚醒。當生成器執行完畢,會丟擲StopIteration異常,供外部send的地方知曉。
因為yield很方便的提供了一套函式掛起,執行的機制,所以我們能夠通過yield來將原本是非同步的流程變成同步的。
gen
tornado有一個gen模組,提供了Task和Callback/Wait機制用來支援同步模型,以task為例:
def MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
@tornado.gen.engine
def get(self):
client = tornado.httpclient.AsyncHTTPClient()
response = yield tornado.gen.Task(client.fetch, "http://www.google.com/")
self.write("Hello World")
self.finish()
可以看到,tornado的gen模組就是通過yield來進行同步化的。主要有如下需要注意的地方:
- 使用gen.engine的decorator,該函式主要就是用來管理generator的流程控制。
- 使用了gen.Task,在gen.Task內部,會生成一個callback函式,傳給async fetch,並執行fetch,因為fetch是一個非同步操作,所以會很快返回。
- 在gen.Task返回之後使用yield,掛起
- 當fetch的callback執行之後,喚醒掛起的流程繼續執行。
可以看到,使用gen和yield之後,原先的非同步邏輯變成了同步流程,在程式碼的閱讀性上面就有不錯的提升,不過對於不熟悉yield的童鞋來說,開始反而會很迷惑,不過只要理解了yield,那就很容易了。
greenlet
雖然yield很強大,但是它只能掛起當前函式,而無法掛起整個堆疊,這個怎麼說呢,譬如我想實現下面的功能:
def a():
yield 1
def b():
a()
t = b()
t.send(None)
這個通過yield是無法實現的,也就是說,a裡面使用yield,它是一個生成器,但是a的掛起無法將b也同時掛起。也就是說,我們需要一套機制,使得堆疊在任何地方都能夠被掛起和恢復,能方便的進行棧切換,而這套機制就是coroutine。
最開始使用coroutine是在lua裡面,它原生提供了coroutine的支援。然後在使用luajit的時候,發現內部是基於fiber(win)和context(unix),也就是說,不光lua,其實c/c++我們也能實現coroutine。現在研究了go,也是內建coroutine,並且這裡極力推薦一篇slide。
python沒有原生提供coroutine,不知道以後會不會有。但有一個greenlet,能幫我們實現coroutine機制。而且還有人專門寫好了tornado與greenlet結合的模組,叫做greenlet_tornado,使用也很簡單
class MainHandler(tornado.web.RequestHandler):
@greenlet_asynchronous
def get(self):
response = greenlet_fetch('http://www.google.com')
self.write("Hello World")
self.finish()
可以看到,使用greenlet,能更方便的實現程式碼邏輯,這點比使用gen更方便,因為這些連寫程式碼的童鞋都不用去糾結yield問題了。
總結
這裡只是簡單的介紹了tornado的一些非同步處理流程,以及將非同步同步化的一些方法。另外,這裡舉得例子都是網路http請求方面的,但是server處理請求的時候,可能還需要進行資料庫,本地檔案的操作,而這些也是同步阻塞耗時操作,同樣可以通過非同步來解決的,這裡就不詳細說明了。