早就想寫一篇文章,總體介紹python的2個異步庫,twisted和tornado。咱們在開發python的tcpserver時候,一般只會用3個庫,twisted、tornado和gevent,其中以twisted和tornado爲表明的異步庫的效率比較高,但對於開發者要求有點高。你們都在討論異步效率高,那到底什麼是異步,爲什麼它的效率比較高呢?世界老是守恆的,異步效率高的同時犧牲了什麼呢?咱們今天就來說講python的異步庫。python
其實咱們談論的異步庫都是基於計算機模型Event Loop,它不僅僅只有python有,若是你們用過ajax就知道,ajax獲取數據的時候,通常都是異步獲取。其實整個js都是基於eventloop的單線程,好吧,扯遠了。那什麼是Eevent Loop呢?請看下圖react
咱們知道,每個程序運行都會開啓一個進程,在tcpserver服務器歷史上,主要有3種方式來處理客戶端來的鏈接。程序員
爲了方便說明,咱們把tcpserver想象成對銀行辦理業務的過程,你每次去銀行辦理業務的時候,其實真正辦理業務的時間並不長,其中不少時候,銀行的工做人員也在等待,好比她操做一筆業務,電腦尚未及時反應過來,她沒事可作,只能等待;打印各類文件的時候,也在等待。這其實跟咱們的tcpserver是同樣的,不少應用,咱們的tcpserver一直在等待。web
第一,阻塞排隊。銀行只開通一個窗口,每一個人過來,都要排隊,每個人都要等待,其中還有不少時候,銀行的工做人員在等電腦、打印機的操做時間。這種方式效率最低下。ajax
第二,子進程。每次來一個客戶,銀行都開啓一個窗口,專門接待,但銀行的窗口不是無限的,每開啓一個窗口,都有代價。這種方式比上面好了一些,但效率還不是那麼高。數據庫
第三,線程。銀行看到每一個業務員雖然一直在忙活,但中間等待時間過長,效率提升不上來。因而,領導規定,每一個業務員同時處理10個客戶(1個進程開始10個線程),在處理客戶1的空餘時間,再處理客戶2,或者其餘的。嗯,貌似效率提升了,但業務員同時接這麼多客戶,極其容易出錯(線程模式,確實容易出錯,並且還很差控制,一般線程都只是處理比較單1、簡單的任務)。flask
好了,通過對歷史問題的研究,銀行終於想到了終極大法,異步。銀行請了機器人作業務員,而且把全部的客戶都圍成一個圈(這個圈就是eventloop),機器人站在這個圈的中間,不停的旋轉(無限循環)。機器人每次接到一個客戶,都讓客戶加入到這個圈子裏。而後就開始處理業務,處理業務,那旋轉暫停,若是在處理這個業務的時候,遇到任何忙等待行爲,好比操做打印機等待、操做電腦時等待,都會先把這個業務掛起來,保存好(保存上下文環境,其實能夠想象成壓棧),而後繼續旋轉,若是有其餘業務過來,處理之,繼續上述行爲。這時候,有個業務等待完畢,發送信號給機器人,機器人把剛纔掛起的這個業務環境(把保存好的上下文環境拉出來,想象成出棧),而後繼續處理,一直處處理完爲止。api
整個過程就是無限循環,遇到事件就處理,若是這個事件須要等待,就掛起,繼續循環,若是等待完畢,發送信號給循環,繼續處理,完畢後,繼續循環。這就是異步。安全
對比歷史的3個過程,異步是否是效率明顯要比以前的高不少?可是也有代價,尤爲對程序員要求比較高,何時該保存上下文?何時出來?出錯的時候,如何處理?等等,這個之後咱們會逐漸介紹這其中的問題。服務器
下面咱們回到實際的twisted,這個圖是官方引用圖,我以爲很是好的詮釋了twisted的運行過程。經過這個圖,再結合我上面的例子,我想你們對twisted的運行過程有個基本瞭解了。
實際上,這個reactor loop就是整合twisted最核心的東西,全部的事件都在這個「圈」上,而在此基礎上,再加上socket,就是接受網絡客戶端數據的過程。這個圈在沒有socket的狀況下,也能夠工做。之後咱們會遇到twisted結合rabbitmq的狀況,rabbitmq的消費者也是一個"圈",其實就是把這個"圈"套在twisted的哪一個"圈"上,只不過twisted的任何事件,都須要異步化。
上面說了這麼多概念,咱們就用代碼試試twisted。我發現網上不少博客開始介紹twisted,每每一大堆代碼,新手都不知道怎麼入手,這對新手來講,是一個難題。咱們今天就嘗試解決這個難題。
from twisted.internet import reactor reactor.run()
代碼如上,就1行代碼,直接運行,這時候這個"圈"就運行起來了。沒有socket,不能接受客戶端寫入數據。
在此基礎上,加一點料。
import time def hello(): print("Hello world!===>" + str(int(time.time()))) from twisted.internet import reactor reactor.callWhenRunning(hello) reactor.callLater(3, hello) reactor.run()
看代碼,我想,你就是不懂twisted,看字面意思,也知道這怎麼回事了吧。callWhenRunning,就是reactor開始運行的時候,就觸發hello函數;callLater就是3秒之後再觸發一次。看一下結果
/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py Hello world!===>1466129667 Hello world!===>1466129670
結果也這樣,是否是很簡單?對,單純的reactor確實很是簡單。咱們多嘗試複雜點的任務看看。
import time def hello(name): print("Hello world!===>" + name + '===>' + str(int(time.time()))) from twisted.internet import reactor, task task1 = task.LoopingCall(hello, 'ding') task1.start(10) reactor.callWhenRunning(hello, 'yudahai') reactor.callLater(3, hello, 'yuyue') reactor.run()
這面在函數裏面,多加了一個參數,又在其中,加了一個循環任務taks1,task1每10秒運行一次。task用twisted會常常用到,由於咱們會輪詢檢測每一個鏈接上來的客戶端意外斷線的狀況,這時候就要用到task。好了,看看結果。
/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py Hello world!===>ding===>1466130033 Hello world!===>yudahai===>1466130033 Hello world!===>yuyue===>1466130036 Hello world!===>ding===>1466130043 Hello world!===>ding===>1466130053 Hello world!===>ding===>1466130063 Hello world!===>ding===>1466130073 Hello world!===>ding===>1466130083 Hello world!===>ding===>1466130093 Hello world!===>ding===>1466130103
看到結果,你們應該對平常twisted這個"圈"會基本使用了吧。
嗯,基本使用會了,但貌似這個很簡單呀,沒有網上所說的,twisted如何難呀?貌似也沒看到中間有任何代價呀?爲何必定要異步呢?爲何中間不能阻塞呢?好吧,上面的例子確實看不出來,咱們來看以下一段代碼,看看阻塞的效果。你們都知道,咱們這邊是不能訪問google網站的,咱們在中間試試訪問google網站,看看效果會咋樣。
import time import requests def hello(name): print("Hello world!===>" + name + '===>' + str(int(time.time()))) def request_google(): res = requests.get('http://www.google.com') return res from twisted.internet import reactor, task reactor.callWhenRunning(hello, 'yudahai') reactor.callLater(1, request_google) reactor.callLater(3, hello, 'yuyue') reactor.run()
我在開始的時候運行一個打印任務,非阻塞,而後1秒以後,發送一個指向google的請求,到第3秒的時候,再執行打印。看看結果
/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py Hello world!===>yudahai===>1466130855 Hello world!===>yuyue===>1466130984 Unhandled Error Traceback (most recent call last): File "/home/yudahai/PycharmProjects/test0001/test001.py", line 21, in <module> reactor.run() File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 1194, in run self.mainLoop() File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 1203, in mainLoop self.runUntilCurrent() --- <exception caught here> --- File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 825, in runUntilCurrent call.func(*call.args, **call.kw) File "/home/yudahai/PycharmProjects/test0001/test001.py", line 10, in request_google res = requests.get('http://www.google.com') File "/usr/local/lib/python3.5/dist-packages/requests/api.py", line 67, in get return request('get', url, params=params, **kwargs) File "/usr/local/lib/python3.5/dist-packages/requests/api.py", line 53, in request return session.request(method=method, url=url, **kwargs) File "/usr/local/lib/python3.5/dist-packages/requests/sessions.py", line 468, in request resp = self.send(prep, **send_kwargs) File "/usr/local/lib/python3.5/dist-packages/requests/sessions.py", line 576, in send r = adapter.send(request, **kwargs) File "/usr/local/lib/python3.5/dist-packages/requests/adapters.py", line 437, in send raise ConnectionError(e, request=request) requests.exceptions.ConnectionError: HTTPConnectionPool(host='www.google.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fc189c69e48>: Failed to establish a new connection: [Errno 101] Network is unreachable',))
看看2個打印之間的間隔,大概相差了130秒,也就是說,中間的130秒,這個程序什麼事都沒有幹,僅僅是等待。固然,我這個例子有點極端,但在實際過程當中,訪問數據庫,訪問網絡,都有可能阻塞住。程序一旦阻塞,效率會極其底下。
那該如何解決呢?這邊有2種方法,一個是用twisted自帶的httpclient進行訪問,twisted自帶的httpclient因爲是異步的,不會阻塞住整個reactor的運行;其次是用線程的方式運行,注意,這裏的線程不是python普通線程,是twisted自帶的線程,它訪問完畢的時候,會發送一個信號給reactor。下面咱們分別用2中方法試試吧。
# coding:utf-8 import time from twisted.web.client import Agent from twisted.web.http_headers import Headers from twisted.internet import reactor, task, defer def hello(name): print("Hello world!===>" + name + '===>' + str(int(time.time()))) @defer.inlineCallbacks def request_google(): agent = Agent(reactor) try: result = yield agent.request('GET', 'http://www.google.com', Headers({'User-Agent': ['Twisted Web Client Example']}), None) except Exception as e: print e return print(result) reactor.callWhenRunning(hello, 'yudahai') reactor.callLater(1, request_google) reactor.callLater(3, hello, 'yuyue') reactor.run()
這就是非阻塞版本的代碼,其中,request返回的是一個延遲對象,因此不會阻塞住reactor,看看結果。
/usr/bin/python2.7 /home/yudahai/PycharmProjects/test0001/test001.py Hello world!===>yudahai===>1466386544 Hello world!===>yuyue===>1466386547 User timeout caused connection failure.
除了訪問google的,其餘的都按時回來,訪問谷歌的並無阻塞reactor。
上面用非阻塞的方式訪問過了,其實在現實過程當中,咱們不少庫沒有非阻塞模式的api,要非阻塞模式,必定要返回twisted的defer對象,若是寫一個庫,還要針對twisted寫一個異步版,這確定強人所難。並且不少時候,哪怕本身的函數,若是不是特別複雜,均可以用線程模式,twisted自己訪問數據庫就是線程模式。咱們來看看線程模式的代碼。
# coding:utf-8 import time import requests from twisted.internet import reactor, task, defer def hello(name): print("Hello world!===>" + name + '===>' + str(int(time.time()))) def request_google(): try: result = requests.get('http://www.google.com', timeout=10) except Exception as e: print e return print(result) reactor.callWhenRunning(hello, 'yudahai') reactor.callInThread(request_google) reactor.callLater(3, hello, 'yuyue') reactor.run()
代碼很簡單,就是把request_google換成線程模式。看看結果。
/usr/bin/python2.7 /home/yudahai/PycharmProjects/test0001/test001.py Hello world!===>yudahai===>1466387418 Hello world!===>yuyue===>1466387421 HTTPConnectionPool(host='www.google.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fc9da0b1ad0>: Failed to establish a new connection: [Errno 101] Network is unreachable',))
是否是也一樣達到目的了?嗯,這時候,你們可能會在想,既然線程也能夠把阻塞代碼線程化,爲啥還直接寫異步代碼呢?異步代碼那麼難寫、難看還容易出錯。
這邊其實有幾個理由,在twisted中,不能大量使用線程。
一、效率問題,若是用線程,咱們幹嗎還用twisted呢?線程會頻繁切換cpu調度,若是大量使用線程,會極大浪費cpu資源,效率會嚴重降低。
二、線程安全,若是第一個問題稍微還有點理由的話,那線程安全問題絕對不能忽視了。好比用twisted接受網絡數據的時候,是非線程安全的,若是用線程模式接受數據,會引發程序崩潰。twisted只有極少數的api支持線程。其實用的最多的例子就是消息隊列的接受系統,不少初級程序員會用線程模式來作消息隊列的接受方式,一開始沒問題,結果運行一段時間之後,就會發現程序不能正常接受數據了,並且還不報錯。twisted官方也建議你們,只要有異步庫,必定優先使用異步庫,線程只是作很是簡單並且不是頻繁的操做。
好了,這章就先講到這,咱們下一章會繼續講twisted作tcpserver,把上一個flask api 系列的項目引進來,作一個聊天系統。