真正的 Tornado 異步非阻塞

其中Tornado的定義是 Web 框架和異步網(wǎng)絡(luò)庫,其中他具備有異步非阻塞能力,能解決他兩個框架請求阻塞的問題,在需要并發(fā)能力時候就應(yīng)該使用Tornado。

但是在實際使用過程中很容易把Tornado使用成異步阻塞框架,這樣對比其他兩大框架沒有任何優(yōu)勢而言,本文就如何實現(xiàn)真正的異步非阻塞記錄。

以下使用的 Python 版本為 2.7.13

平臺為 Macbook Pro 2016

使用 gen.coroutine 異步編程

在 Tornado 中兩個裝飾器:

tornado.web.asynchronous

tornado.gen.coroutine

asynchronous 裝飾器是讓請求變成長連接的方式,必須手動調(diào)用self.finish()才會響應(yīng)

class MainHandler(tornado.web.RequestHandler):

????@tornado.web.asynchronous

????def get(self):# bad?

????????self.write("Hello, world")

asynchronous 裝飾器不會自動調(diào)用self.finish(),如果沒有沒有指定結(jié)束,該長連接會一直保持直到 pending 狀態(tài)。

所以正確是使用方式是使用了 asynchronous 需要手動 finish

class MainHandler(tornado.web.RequestHandler):

????@tornado.web.asynchronous????

????def get(self):

????????self.write("Hello, world")

????????self.finish()

coroutine 裝飾器是指定改請求為協(xié)程模式,說明白點就是能使用yield配合 Tornado 編寫異步程序。

Tronado 為協(xié)程實現(xiàn)了一套自己的協(xié)議,不能使用 Python 普通的生成器。

在使用協(xié)程模式編程之前要知道如何編寫 Tornado 中的異步函數(shù),Tornado 提供了多種的異步編寫形式:回調(diào)、Future、協(xié)程等,其中以協(xié)程模式最是簡單和用的最多。

編寫一個基于協(xié)程的異步函數(shù)同樣需要 coroutine 裝飾器

@gen.coroutine

def sleep(self):

????yield gen.sleep(10)

????raise gen.Return([1,2,3,4,5])

這就是一個異步函數(shù),Tornado 的協(xié)程異步函數(shù)有兩個特點:

需要使用 coroutine 裝飾器

返回值需要使用raise gen.Return()當(dāng)做異常拋出

返回值作為異常拋出是因為在 Python 3.2 之前生成器是不允許有返回值的。

使用過 Python 生成器應(yīng)該知道,想要啟動生成器的話必須手動執(zhí)行next()方法才行,所以這里的 coroutine 裝飾器的其中一個作用就是在調(diào)用這個異步函數(shù)時候自動執(zhí)行生成器。

使用 coroutine 方式有個很明顯是缺點就是嚴(yán)重依賴第三方庫的實現(xiàn),如果庫本身不支持 Tornado 的異步操作再怎么使用協(xié)程也是白搭依然會是阻塞的,放個例子感受一下。

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()

為了顯示更明顯設(shè)置了 10 秒

當(dāng)我們使用yield gen.sleep(10)這個異步的 sleep 時候其他請求是不阻塞的。

當(dāng)使用time.sleep(10)時候會阻塞其他的請求。

這里的異步非阻塞是針對另一請求來說的,本次的請求該是阻塞的仍然是阻塞的。

gen.coroutine在 Tornado 3.1 后會自動調(diào)用self.finish()結(jié)束請求,可以不使用asynchronous裝飾器。

所以這種實現(xiàn)異步非阻塞的方式需要依賴大量的基于 Tornado 協(xié)議的異步庫,使用上比較局限,好在還是有一些可以用的異步庫

基于線程的異步編程

使用gen.coroutine裝飾器編寫異步函數(shù),如果庫本身不支持異步,那么響應(yīng)任然是阻塞的。

在 Tornado 中有個裝飾器能使用ThreadPoolExecutor來讓阻塞過程編程非阻塞,其原理是在 Tornado 本身這個線程之外另外啟動一個線程來執(zhí)行阻塞的程序,從而讓 Tornado 變得阻塞。

futures 在 Python3 是標(biāo)準(zhǔn)庫,但是在 Python2 中需要手動安裝

pip install futures

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是對標(biāo)準(zhǔn)庫中的 threading 的高度封裝,利用線程的方式讓阻塞函數(shù)異步化,解決了很多庫是不支持異步的問題。

但是與之而來的問題是,如果大量使用線程化的異步函數(shù)做一些高負(fù)載的活動,會導(dǎo)致該 Tornado 進(jìn)程性能低下響應(yīng)緩慢,這只是從一個問題到了另一個問題而已。

所以在處理一些小負(fù)載的工作,是能起到很好的效果,讓 Tornado 異步非阻塞的跑起來。

但是明明知道這個函數(shù)中做的是高負(fù)載的工作,那么你應(yīng)該采用另一種方式,使用 Tornado 結(jié)合 Celery 來實現(xiàn)異步非阻塞。

基于 Celery 的異步編程

Celery 是一個簡單、靈活且可靠的,處理大量消息的分布式系統(tǒng),專注于實時處理的任務(wù)隊列,同時也支持任務(wù)調(diào)度。

Celery 并不是唯一選擇,你可選擇其他的任務(wù)隊列來實現(xiàn),但是 Celery 是 Python 所編寫,能很快的上手,同時 Celery 提供了優(yōu)雅的接口,易于與 Python Web 框架集成等特點。

與 Tornado 的配合可以使用tornado-celery,該包已經(jīng)把 Celery 封裝到 Tornado 中,可以直接使用。

實際測試中,由于 tornado-celery 很久沒有更新,導(dǎo)致請求會一直阻塞,不會返回

解決辦法是:

把 celery 降級到 3.1pip install celery==3.1

把 pika 降級到 0.9.14pip 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 運(yùn)行在另一個進(jìn)程中,獨(dú)立于 Tornado 進(jìn)程,不會影響 Tornado 運(yùn)行效率,在處理復(fù)雜任務(wù)時候比進(jìn)程模式更有效率。

總結(jié)

方法優(yōu)點缺點可用性

gen.coroutine簡單、優(yōu)雅需要異步庫支持★★☆☆☆

線程簡單可能會影響性能★★★☆☆

Celery性能好操作復(fù)雜、版本低★★★☆☆

目前沒有找到最佳的異步非阻塞的編程模式,可用的異步庫比較局限,只有經(jīng)常用的,個人編寫異步庫比較困難。

推薦使用線程和 Celery 的模式進(jìn)行異步編程,輕量級的放在線程中執(zhí)行,復(fù)雜的放在 Celery 中執(zhí)行。當(dāng)然如果有異步庫使用那最好不過了。

Python 3 中可以把 Tornado 設(shè)置為 asyncio 的模式,這樣就使用 兼容 asyncio 模式的庫,這應(yīng)該是日后的方向。

Reference

http://www.tornadoweb.org/en/stable/

https://github.com/mher/tornado-celery

https://github.com/tornadoweb/tornado/wiki/Links

轉(zhuǎn)自:https://hexiangyu.me/2017/01/29/real-tornado-async-noblocking/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容