1. 程式人生 > 實用技巧 >tornado - 非同步非阻塞

tornado - 非同步非阻塞

目錄

阻塞式IO框架 (Django、Flask)

大多數的web框架都是阻塞式的,如果一個請求到達服務端且為處理完該請求,後序請求會一致等待。

常用的解決方案:開啟多執行緒/多程序,提高併發。 但是比較浪費系統資源

tornado多程序模式(僅linux):

import tornado.ioloop
import time
import tornado.web
from tornado.httpserver import HTTPServer

class IndexHadlar(tornado.web.RequestHandler):
    def get(self):
        print('請求開始')
        time.sleep(10)
        self.write('hello,world ')
        print("請求結束")
application=tornado.web.Application([
    (r'/index',IndexHadlar)
])


if __name__ == '__main__':
    # 單執行緒模式
    # application.listen(8888)
    # tornado.ioloop.IOLoop.instance().start()
    
    # 多執行緒模式
    server=HTTPServer(application)
    server.bind(8888)
    server.start(3) #開啟4個程序
    tornado.ioloop.IOLoop.instance().start()

Tornado非同步非阻塞

非同步非阻塞就是在服務端結合IO多路複用select/poll/epoll模板,做到1個執行緒在遇到IO操作的情況下,還可以做一些其他的任務(協程化);Tornado預設是阻塞的同時也支援非同步非阻塞功能。

處理流程:

1.客戶端傳送請求如果請求內容不涉及IO操作(連線資料、還得去其他網站獲取內容)服務端直接響應客戶端;

2.如果請求內容涉及IO操作,服務端把本次連線的socket資訊新增到socket監聽列表epoll中監聽起來;

然後去連線其它socket(資料庫、其它站點)由於是不阻塞的所以服務端把這次傳送socket資訊也監聽起來;(一直迴圈監聽epoll,直到socket監聽列表中的socket發生變化)

3.把socket全部監聽之後,就可以去繼續接收其它請求了,如果檢測到socket監聽列表中的socket有變化(有資料返回),找到對應socket響應資料,並從socket監聽列表中剔除;

小結:

Tornado的非同步非阻塞,本質上是請求到達檢視 1、先yield 一個Future物件 2、 IO多路複用模組把該socket新增到監聽列表epoll迴圈監聽起來;3、 迴圈監聽過程中哪1個socket發生變化有response,執行 Future.set_result(response),請求至此返回結束,否則socket連線一直不斷開,IO多路複用模組epoll一直迴圈監聽socket是否發生變化。

非阻塞模式:

當傳送GET請求時,由於方法被@gen.coroutine裝飾且yield 一個 Future物件,那麼Tornado會等待,等待使用者向Future物件中放置資料或者傳送訊號,如果獲取到資料或訊號之後,就開始執行doing方法。

非同步非阻塞體現在當在Tornaod等待使用者向Future物件中放置資料時,還可以處理其他請求。

注意:在等待使用者向Future物件中放置資料或訊號時,此連線是不斷開的。

import tornado.ioloop
import time
import tornado.web
from tornado import gen             #匯入
from tornado.concurrent import Future

class IndexHadlar(tornado.web.RequestHandler):
    
    @gen.coroutine	# coroutine(協程裝飾器)
    def get(self):
        print('請求開始')
        future=Future()
        tornado.ioloop.IOLoop.current().add_timeout(time.time()+10,self.doing)
        yield future #yield 1個future物件,IO之後自動切換到doing方法執行;

    def doing(self):
        self.write('請求完成')
        self.finish()           #關閉連線


application=tornado.web.Application([
    (r'/index/',IndexHadlar)
])


if __name__ == '__main__':
    # 單程序模式
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Tornado httpclient:

如果服務端接受到客戶端的請求,需要去其他API獲取資料,再響應給客戶端,這就涉及到了IO操作,Tornado提供了httpclient類庫用於傳送Http請求,其配合Tornado的非同步非阻塞使用。

import tornado.web
from tornado import gen
from tornado import httpclient

class AsyncHandler(RequestHandler):
    
    @gen.coroutine	# 協程
    def get(self):
        print('請求開始')
        http = httpclient.AsyncHTTPClient()
        
        yield http.fetch('https://baidu.com',self.done)	# 獲取到這個地址的資料或者訊號後,就開始執行done方法

    def done(self,respose,*args,**kwargs):
        # print(respose)	# 獲取yield的結果
        self.write(respose.body)
        self.finish()


application = tornado.web.Application([
    (r"/zhanggen/", AsyncHandler),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

使用非同步物件處理耗時操作,比如上面的AsyncHTTPClient。

呼叫yield關鍵字獲取非同步物件的處理結果,self.done是一個callback回撥函式。

非同步非阻塞體現在當在Tornaod等待請求的資料返回時,還可以處理其他請求。

Tornado-MySQL類庫

如果服務端接收到客戶端請求,需要連線資料庫再把查詢的結果響應客戶端,這個過程中連線資料、傳送查詢SQL、接收資料庫返回結果 都會遇到IO阻塞、耗時的問題,所以Tornado提供了Tornado-MySQL模組(對PyMySQL進行二次封裝),讓我們在使用資料庫的時候也可以做到非同步非阻塞。

方式一:

需要對每個IO操作分別yeild,操作起來比較繁瑣。

# yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))

方式二:可以通過task的方式把IO操作封裝到函式中統一進行非同步處理(無論什麼方式本質都會yelid 1個Future物件);

"""
需要先安裝支援非同步操作Mysql的類庫:
    Tornado-MySQL: https://github.com/PyMySQL/Tornado-MySQL#installation

    pip3 install Tornado-MySQL

"""

import tornado.web
from tornado import gen

import tornado_mysql
from tornado_mysql import pools

POOL = pools.Pool(
    dict(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb'),
    max_idle_connections=1,
    max_recycle_sec=3)

@gen.coroutine
def get_user_by_conn_pool(user):
    cur = yield POOL.execute("SELECT SLEEP(%s)", (user,))
    row = cur.fetchone()
    raise gen.Return(row)

@gen.coroutine
def get_user(user):
    conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', passwd='123', db='cmdb',charset='utf8')
    cur = conn.cursor()
    # yield cur.execute("SELECT name,email FROM web_models_userprofile where name=%s", (user,))
    yield cur.execute("select sleep(10)")
    row = cur.fetchone()
    cur.close()
    conn.close()
    raise gen.Return(row)


class LoginHandler(tornado.web.RequestHandler):
    def get(self, *args, **kwargs):
        self.render('login.html')

    @gen.coroutine
    def post(self, *args, **kwargs):
        user = self.get_argument('user')
        data = yield gen.Task(get_user, user)  #把函式新增任務
        if data:
            print(data)
            self.redirect('http://www.xxx.com')
        else:
            self.render('login.html')


application = tornado.web.Application([
    (r"/login", LoginHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()