其中 Tornado 的定義是 Web 框架和異步網絡庫,其中他具有有異步非阻塞能力,能解決他兩個框架請求阻塞的問題,在須要併發能力時候就應該使用 Tornado。python
可是在實際使用過程當中很容易把 Tornado 使用成異步阻塞框架,這樣對比其餘兩大框架沒有任何優點而言,本文就如何實現真正的異步非阻塞記錄。git
如下使用的 Python 版本爲 2.7.13github
平臺爲 Macbook Pro 2016web
在 Tornado 中兩個裝飾器:編程
asynchronous 裝飾器是讓請求變成長鏈接的方式,必須手動調用 self.finish()
纔會響應網絡
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
# bad
self.write("Hello, world")複製代碼
asynchronous 裝飾器不會自動調用self.finish()
,若是沒有沒有指定結束,該長鏈接會一直保持直到 pending 狀態。併發
因此正確是使用方式是使用了 asynchronous 須要手動 finishapp
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.write("Hello, world")
self.finish()複製代碼
coroutine 裝飾器是指定改請求爲協程模式,說明白點就是能使用 yield
配合 Tornado 編寫異步程序。框架
Tronado 爲協程實現了一套本身的協議,不能使用 Python 普通的生成器。異步
在使用協程模式編程以前要知道如何編寫 Tornado 中的異步函數,Tornado 提供了多種的異步編寫形式:回調、Future、協程等,其中以協程模式最是簡單和用的最多。
編寫一個基於協程的異步函數一樣須要 coroutine 裝飾器
@gen.coroutine
def sleep(self):
yield gen.sleep(10)
raise gen.Return([1, 2, 3, 4, 5])複製代碼
這就是一個異步函數,Tornado 的協程異步函數有兩個特色:
raise gen.Return()
當作異常拋出返回值做爲異常拋出是由於在 Python 3.2 以前生成器是不容許有返回值的。
使用過 Python 生成器應該知道,想要啓動生成器的話必須手動執行 next()
方法才行,因此這裏的 coroutine 裝飾器的其中一個做用就是在調用這個異步函數時候自動執行生成器。
使用 coroutine 方式有個很明顯是缺點就是嚴重依賴第三方庫的實現,若是庫自己不支持 Tornado 的異步操做再怎麼使用協程也是白搭依然會是阻塞的,放個例子感覺一下。
import time
import logging
import tornado.ioloop
import tornado.web
import tornado.options
from tornado import gen
tornado.options.parse_command_line()
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.write("Hello, world")
self.finish()
class NoBlockingHnadler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
yield gen.sleep(10)
self.write('Blocking Request')
class BlockingHnadler(tornado.web.RequestHandler):
def get(self):
time.sleep(10)
self.write('Blocking Request')
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/block", BlockingHnadler),
(r"/noblock", NoBlockingHnadler),
], autoreload=True)
if __name__ == "__main__":
app = make_app()
app.listen(8000)
tornado.ioloop.IOLoop.current().start()複製代碼
爲了顯示更明顯設置了 10 秒
當咱們使用 yield gen.sleep(10)
這個異步的 sleep 時候其餘請求是不阻塞的。
當使用 time.sleep(10)
時候會阻塞其餘的請求。
這裏的異步非阻塞是針對另外一請求來講的,本次的請求該是阻塞的仍然是阻塞的。
gen.coroutine
在 Tornado 3.1 後會自動調用 self.finish()
結束請求,能夠不使用 asynchronous
裝飾器。
因此這種實現異步非阻塞的方式須要依賴大量的基於 Tornado 協議的異步庫,使用上比較侷限,好在仍是有一些能夠用的異步庫
使用 gen.coroutine
裝飾器編寫異步函數,若是庫自己不支持異步,那麼響應任然是阻塞的。
在 Tornado 中有個裝飾器能使用 ThreadPoolExecutor
來讓阻塞過程編程非阻塞,其原理是在 Tornado 自己這個線程以外另外啓動一個線程來執行阻塞的程序,從而讓 Tornado 變得阻塞。
futures 在 Python3 是標準庫,可是在 Python2 中須要手動安裝
pip install futures
import time
import logging
import tornado.ioloop
import tornado.web
import tornado.options
from tornado import gen
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
tornado.options.parse_command_line()
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.write("Hello, world")
self.finish()
class NoBlockingHnadler(tornado.web.RequestHandler):
executor = ThreadPoolExecutor(4)
@run_on_executor
def sleep(self, second):
time.sleep(second)
return second
@gen.coroutine
def get(self):
second = yield self.sleep(5)
self.write('noBlocking Request: {}'.format(second))
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/noblock", NoBlockingHnadler),
], autoreload=True)
if __name__ == "__main__":
app = make_app()
app.listen(8000)
tornado.ioloop.IOLoop.current().start()複製代碼
ThreadPoolExecutor
是對標準庫中的 threading 的高度封裝,利用線程的方式讓阻塞函數異步化,解決了不少庫是不支持異步的問題。
可是與之而來的問題是,若是大量使用線程化的異步函數作一些高負載的活動,會致使該 Tornado 進程性能低下響應緩慢,這只是從一個問題到了另外一個問題而已。
因此在處理一些小負載的工做,是能起到很好的效果,讓 Tornado 異步非阻塞的跑起來。
可是明明知道這個函數中作的是高負載的工做,那麼你應該採用另外一種方式,使用 Tornado 結合 Celery 來實現異步非阻塞。
Celery 是一個簡單、靈活且可靠的,處理大量消息的分佈式系統,專一於實時處理的任務隊列,同時也支持任務調度。
Celery 並非惟一選擇,你可選擇其餘的任務隊列來實現,可是 Celery 是 Python 所編寫,能很快的上手,同時 Celery 提供了優雅的接口,易於與 Python Web 框架集成等特色。
與 Tornado 的配合可使用 tornado-celery
,該包已經把 Celery 封裝到 Tornado 中,能夠直接使用。
實際測試中,因爲 tornado-celery 好久沒有更新,致使請求會一直阻塞,不會返回
解決辦法是:
- 把 celery 降級到 3.1
pip install celery==3.1
- 把 pika 降級到 0.9.14
pip install pika==0.9.14
import time
import logging
import tornado.ioloop
import tornado.web
import tornado.options
from tornado import gen
import tcelery, tasks
tornado.options.parse_command_line()
tcelery.setup_nonblocking_producer()
class MainHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.write("Hello, world")
self.finish()
class CeleryHandler(tornado.web.RequestHandler):
@gen.coroutine
def get(self):
response = yield gen.Task(tasks.sleep.apply_async, args=[5])
self.write('CeleryBlocking Request: {}'.format(response.result))
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/celery-block", CeleryHandler),
], autoreload=True)
if __name__ == "__main__":
app = make_app()
app.listen(8000)
tornado.ioloop.IOLoop.current().start()複製代碼
import os
import time
from celery import Celery
from tornado import gen
celery = Celery("tasks", broker="amqp://")
celery.conf.CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'amqp')
@celery.task
def sleep(seconds):
time.sleep(float(seconds))
return seconds
if __name__ == "__main__":
celery.start()複製代碼
Celery 的 Worker 運行在另外一個進程中,獨立於 Tornado 進程,不會影響 Tornado 運行效率,在處理複雜任務時候比進程模式更有效率。
方法 | 優勢 | 缺點 | 可用性 |
---|---|---|---|
gen.coroutine | 簡單、優雅 | 須要異步庫支持 | ★★☆☆☆ |
線程 | 簡單 | 可能會影響性能 | ★★★☆☆ |
Celery | 性能好 | 操做複雜、版本低 | ★★★☆☆ |
目前沒有找到最佳的異步非阻塞的編程模式,可用的異步庫比較侷限,只有常常用的,我的編寫異步庫比較困難。
推薦使用線程和 Celery 的模式進行異步編程,輕量級的放在線程中執行,複雜的放在 Celery 中執行。固然若是有異步庫使用那最好不過了。
Python 3 中能夠把 Tornado 設置爲 asyncio 的模式,這樣就使用 兼容 asyncio 模式的庫,這應該是往後的方向。