學習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處理請求的時候,可能還需要進行數據庫,本地文件的操作,而這些也是同步阻塞耗時操作,同樣可以通過異步來解決的,這裏就不詳細說明了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章