使用tornado的coroutine進行程式設計

pythontab發表於2013-06-21

在tornado3釋出之後,強化了coroutine的概念,在非同步程式設計中,替代了原來的gen.engine, 變成現在的gen.coroutine。這個裝飾器本來就是為了簡化在tornado中的非同步程式設計。避免寫回撥函式, 使得開發起來更加符合正常邏輯思維。一個簡單的例子如下:


class MaindHandler(web.RequestHandler):

   @asynchronous

   @gen.coroutine

   def post(self):

       client = AsyncHTTPClient()

       resp = yield client.fetch(https://api.github.com/users")

       if resp.code == 200:

           resp = escape.json_decode(resp.body)

           self.write(json.dumps(resp, indent=4, separators=(',', ':')))

       else:

           resp = {"message": "error when fetch something"}

           self.write(json.dumps(resp, indent=4, separators={',', ':')))

       self.finish()

在yield語句之後,ioloop將會註冊該事件,等到resp返回之後繼續執行。這個過程是非同步的。在這裡使用json.dumps,而沒有使用tornado自帶的escape.json_encode,是因為在構建REST風格的API的時候,往往會從瀏覽器裡訪問獲取JSON格式的資料。使用json.dumps格式化資料之後,在瀏覽器端顯示檢視的時候會更加友好。Github API就是這一風格的使用者。其實escape.json_encode就是對json.dumps的簡單包裝,我在提pull request要求包裝更多功能的時候,作者的回答escape並不打算提供全部的json功能,使用者可以自己直接使用json模組。


Gen.coroutine原理


在之前一篇部落格中講到要使用tornado的非同步特性,必須使用非同步的庫。否則單個程式阻塞,根本不會達到非同步的效果。 Tornado的非同步庫中最常用的就是自帶的AsyncHTTPClient,以及在其基礎上實現的OpenID登入驗證介面。另外更多的非同步庫可以在這裡找到。包括用的比較多的MongoDB的Driver。


在3.0版本之後,gen.coroutine模組顯得比較突出。coroutine裝飾器可以讓本來靠回撥的非同步程式設計看起來像同步程式設計。其中便是利用了Python中生成器的Send函式。在生成器中,yield關鍵字往往會與正常函式中的return相比。它可以被當成迭代器,從而使用next()返回yield的結果。但是生成器還有另外一個用法,就是使用send方法。在生成器內部可以將yield的結果賦值給一個變數,而這個值是透過外部的生成器client來send的。舉一個例子:


def test_yield():

   pirnt "test yeild"

   says = (yield)

   print says


if __name__ == "__main__":

   client = test_yield()

   client.next()

   client.send("hello world")

輸出結果如下:


test yeild

hello world

已經在執行的函式會掛起,直到呼叫它的client使用send方法,原來函式繼續執行。而這裡的gen.coroutine方法就是非同步執行需要的操作,然後等待結果返回之後,再send到原函式,原函式則會繼續執行,這樣就以同步方式寫的程式碼達到了非同步執行的效果。


Tornado非同步程式設計


使用coroutine實現函式分離的非同步程式設計。具體如下:


@gen.coroutine

def post(self):

   client = AsyncHTTPClient()

   resp = yield client.fetch("https://api.github.com/users")

   if resp == 200:

       body = escape.json_decode(resy.body)

   else:

       body = {"message": "client fetch error"}

       logger.error("client fetch error %d, %s" % (resp.code, resp.message))

   self.write(escape.json_encode(body))

   self.finish()

換成函式之後可以變成這樣;


@gen.coroutime

def post(self):

   resp = yield GetUser()

   self.write(resp)


@gen.coroutine

def GetUser():

   client = AsyncHTTPClient()

   resp = yield client.fetch("https://api.github.com/users")

   if resp.code == 200:

       resp = escape.json_decode(resp.body)

   else:

       resp = {"message": "fetch client error"}

       logger.error("client fetch error %d, %s" % (resp.code, resp.message))

   raise gen.Return(resp)

這裡,當把非同步封裝在一個函式中的時候,並不是像普通程式那樣使用return關鍵字進行返回,gen模組提供了一個gen.Return的方法。是透過raise方法實現的。這個也是和它是使用生成器方式實現有關的。


使用coroutine跑定時任務


Tornado中有這麼一個方法:


tornado.ioloop.IOLoop.instance().add_timeout()

該方法是time.sleep的非阻塞版本,它接受一個時間長度和一個函式這兩個引數。表示多少時間之後呼叫該函式。在這裡它是基於ioloop的,因此是非阻塞的。該方法在客戶端長連線以及回撥函式程式設計中使用的比較多。但是用它來跑一些定時任務卻是無奈之舉。通常跑定時任務也沒必要使用到它。但是我在使用heroku的時候,發現沒有註冊信用卡的話僅僅能夠使用一個簡單Web Application的託管。不能新增定時任務來跑。於是就想出這麼一個方法。在這裡,我主要使用它隔一段時間透過Github API介面去抓取資料。大自使用方法如下:


裝飾器


 def sync_loop_call(delta=60 * 1000):

 """

 Wait for func down then process add_timeout

 """

     def wrap_loop(func):

         @wraps(func)

         @gen.coroutine

         def wrap_func(*args, **kwargs):

             options.logger.info("function %r start at %d" %

                                 (func.__name__, int(time.time())))

             try:

                 yield func(*args, **kwargs)

             except Exception, e:

                 options.logger.error("function %r error: %s" %

                                      (func.__name__, e))

             options.logger.info("function %r end at %d" %

                                 (func.__name__, int(time.time())))

             tornado.ioloop.IOLoop.instance().add_timeout(

                 datetime.timedelta(milliseconds=delta),

                 wrap_func)

         return wrap_func

     return wrap_loop

任務函式


 @sync_loop_call(delta=10 * 1000)

 def worker():

     """

     Do something

     """

新增任務


 if __name__ == "__main__":

 worker()

 app.listen(options.port)

 tornado.ioloop.IOLoop.instance().start()

這樣做之後,當Web Application啟動之後,定時任務就會隨著跑起來,而且因為它是基於事件的,並且非同步執行的,所以並不會影響Web服務的正常執行,當然任務不能是阻塞的或計算密集型的。我這裡主要是抓取資料,而且用的是Tornado自帶的非同步抓取方法。


在sync_loop_call裝飾器中,我在wrap_func函式上加了@gen.coroutine裝飾器,這樣就保證只有yeild的函式執行完之後,才會執行add_timeout操作。如果沒有@gen.coroutine裝飾器。那麼不等到yeild返回,就會執行add_timeout了。


完整地例子可以參見我的Github,這個專案搭建在heroku上。用於展示Github使用者活躍度排名和使用者區域分佈情況。可以訪問Github-Data檢視。由於國內heroku被牆,需要翻牆才能訪問。


總結


Tornado是一個非阻塞的web伺服器以及web框架,但是在使用的時候只有使用非同步的庫才會真正發揮它非同步的優勢,當然有些時候因為App本身要求並不是很高,如果不是阻塞特別嚴重的話,也不會有問題。另外使用coroutine模組進行非同步程式設計的時候,當把一個功能封裝到一個函式中時,在函式執行中,即使出現錯誤,如果沒有去捕捉的話也不會丟擲,這在除錯上顯得非常困難。


相關文章