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