在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模塊進行異步編程的時候,當把一個功能封裝到一個函數中時,在函數運行中,即使出現錯誤,如果沒有去捕捉的話也不會拋出,這在調試上顯得非常困難。