我們在開發python的tcpserver時候,通常只會用3個庫,twisted、tornado和gevent,其中以twisted和tornado爲代表的異步庫的效率比較高,但對於開發者要求有點高。大家都在討論異步效率高,那到底什麼是異步,爲何它的效率比較高呢?世界總是守恆的,異步效率高的同時犧牲了什麼呢?我們今天就來講講python的異步庫。
其實我們談論的異步庫都是基於計算機模型Event Loop,它不單單隻有python有,如果大家用過ajax就知道,ajax獲取數據的時候,一般都是異步獲取。其實整個js都是基於eventloop的單線程,好吧,扯遠了。那什麼是Eevent Loop呢?請看下圖
我們知道,每一個程序運行都會開啓一個進程,在tcpserver服務器歷史上,主要有3種方式來處理客戶端來的連接。
爲了方便說明,我們把tcpserver想象成對銀行辦理業務的過程,你每次去銀行辦理業務的時候,其實真正辦理業務的時間並不長,其中很多時候,銀行的工作人員也在等待,比如她操作一筆業務,電腦還沒有及時反應過來,她沒事可做,只能等待;打印各種文件的時候,也在等待。這其實跟我們的tcpserver是一樣的,很多應用,我們的tcpserver一直在等待。
第一,阻塞排隊。銀行只開通一個窗口,每個人過來,都要排隊,每一個人都要等待,其中還有很多時候,銀行的工作人員在等電腦、打印機的操作時間。這種方式效率最低下。
第二,子進程。每次來一個客戶,銀行都開啓一個窗口,專門接待,但銀行的窗口不是無限的,每開啓一個窗口,都有代價。這種方式比上面好了一些,但效率還不是那麼高。
第三,線程。銀行看到每個業務員雖然一直在忙活,但中間等待時間過長,效率提高不上來。於是,領導規定,每個業務員同時處理10個客戶(1個進程開始10個線程),在處理客戶1的空餘時間,再處理客戶2,或者其他的。嗯,貌似效率提高了,但業務員同時接這麼多客戶,極其容易出錯(線程模式,確實容易出錯,而且還不好控制,通常線程都只是處理比較單一、簡單的任務)。
好了,經過對歷史問題的研究,銀行終於想到了終極大法,異步。銀行請了機器人做業務員,並且把所有的客戶都圍成一個圈(這個圈就是eventloop),機器人站在這個圈的中間,不停的旋轉(無限循環)。機器人每次接到一個客戶,都讓客戶加入到這個圈子裏。然後就開始處理業務,處理業務,那旋轉暫停,如果在處理這個業務的時候,遇到任何忙等待行爲,比如操作打印機等待、操作電腦時等待,都會先把這個業務掛起來,保存好(保存上下文環境,其實可以想象成壓棧),然後繼續旋轉,如果有其他業務過來,處理之,繼續上述行爲。這時候,有個業務等待完畢,發送信號給機器人,機器人把剛纔掛起的這個業務環境(把保存好的上下文環境拉出來,想象成出棧),然後繼續處理,一直到處理完爲止。
整個過程就是無限循環,遇到事件就處理,如果這個事件需要等待,就掛起,繼續循環,如果等待完畢,發送信號給循環,繼續處理,完畢後,繼續循環。這就是異步。
對比歷史的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中,不能大量使用線程。
1、效率問題,如果用線程,我們幹嘛還用twisted呢?線程會頻繁切換cpu調度,如果大量使用線程,會極大浪費cpu資源,效率會嚴重下降。
2、線程安全,如果第一個問題稍微還有點理由的話,那線程安全問題絕對不能忽視了。比如用twisted接受網絡數據的時候,是非線程安全的,如果用線程模式接受數據,會引起程序崩潰。twisted只有極少數的api支持線程。其實用的最多的例子就是消息隊列的接受系統,很多初級程序員會用線程模式來做消息隊列的接受方式,一開始沒問題,結果運行一段時間以後,就會發現程序不能正常接受數據了,而且還不報錯。twisted官方也建議大家,只要有異步庫,一定優先使用異步庫,線程只是做非常簡單而且不是頻繁的操作。