使用tornado的gen.coroutine進行異步編程


在tornado3發布之后,強化了coroutine的概念,在異步編程中,替代了原來的gen.engine, 變成現在的gen.coroutine。這個裝飾器本來就是為了簡化在tornado中的異步編程。避免寫回調函數, 使得開發起來更加符合正常邏輯思維。


一個簡單的例子如下:
  1. class MaindHandler(web.RequestHandler):
  2. @asynchronous
  3. @gen.coroutine
  4. def post(self):
  5. client = AsyncHTTPClient()
  6. resp = yield client.fetch(https://api.github.com/users")
  7. if resp.code == 200:
  8. resp = escape.json_decode(resp.body)
  9. self.write(json.dumps(resp, indent=4, separators=(',', ':')))
  10. else:
  11. resp = {"message": "error when fetch something"}
  12. self.write(json.dumps(resp, indent=4, separators={',', ':')))
  13. 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的。舉一個例子:

  1. def test_yield():
  2. pirnt "test yeild"
  3. says = (yield)
  4. print says
  5.  
  6. if __name__ == "__main__":
  7. client = test_yield()
  8. client.next()
  9. client.send("hello world")

輸出結果如下:
test yeild
hello world

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

Tornado異步編程

使用coroutine實現函數分離的異步編程。具體如下:
  1. @gen.coroutine
  2. def post(self):
  3. client = AsyncHTTPClient()
  4. resp = yield client.fetch("https://api.github.com/users")
  5. if resp == 200:
  6. body = escape.json_decode(resy.body)
  7. else:
  8. body = {"message": "client fetch error"}
  9. logger.error("client fetch error %d, %s" % (resp.code, resp.message))
  10. self.write(escape.json_encode(body))
  11. self.finish()

換成函數之后可以變成這樣;
  1. @gen.coroutime
  2. def post(self):
  3. resp = yield GetUser()
  4. self.write(resp)
  5.  
  6. @gen.coroutine
  7. def GetUser():
  8. client = AsyncHTTPClient()
  9. resp = yield client.fetch("https://api.github.com/users")
  10. if resp.code == 200:
  11. resp = escape.json_decode(resp.body)
  12. else:
  13. resp = {"message": "fetch client error"}
  14. logger.error("client fetch error %d, %s" % (resp.code, resp.message))
  15. 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接口去抓取數據。大自使用方法如下:

裝飾器
  1. def sync_loop_call(delta=60 * 1000):
  2. """
  3. Wait for func down then process add_timeout
  4. """
  5. def wrap_loop(func):
  6. @wraps(func)
  7. @gen.coroutine
  8. def wrap_func(*args, **kwargs):
  9. options.logger.info("function %r start at %d" %
  10. (func.__name__, int(time.time())))
  11. try:
  12. yield func(*args, **kwargs)
  13. except Exception, e:
  14. options.logger.error("function %r error: %s" %
  15. (func.__name__, e))
  16. options.logger.info("function %r end at %d" %
  17. (func.__name__, int(time.time())))
  18. tornado.ioloop.IOLoop.instance().add_timeout(
  19. datetime.timedelta(milliseconds=delta),
  20. wrap_func)
  21. return wrap_func
  22. return wrap_loop

任務函數
  1. @sync_loop_call(delta=10 * 1000)
  2. def worker():
  3. """
  4. Do something
  5. """

添加任務
  1. if __name__ == "__main__":
  2. worker()
  3. app.listen(options.port)
  4. 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模塊進行異步編程的時候,當把一個功能封裝到一個函數中時,在函數運行中,即使出現錯誤,如果沒有去捕捉的話也不會拋出,這在調試上顯得非常困難。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM