前言
最近有人在Twisted郵件列表中提出諸如"為任務緊急的人提供一份Twisted介紹"的需求。值得提前透露的是,這個系列并不會如他們所愿。尤其是介紹Twisted框架和基于Python 的異步編程而言,可能短時間無法講清楚。因此,如果你時間緊急,這恐怕不是你想找的資料。
我相信如果對異步編程模型一無所知,快速的介紹同樣無法讓你對其有所理解,至少你得稍微懂點基礎知識吧。我已經用Twisted框架幾年了,因此思考過我當初是怎么學習它(學得很慢)并發現學習它的最大難度并不在Twisted本身,而在于對其模型的理解,只有理解了這個模型,你才能更好去寫和理解異步程序的代碼。大部分Twisted的代碼寫得很清晰,其在線文檔也非常棒(至少在開源軟件這個層次上可以這么說)。但如果不理解這個模型,不管是讀Twisted源碼還是使用Twisted的代碼更或者是相關文檔,你都會感到非常的傷腦筋。
因此,我會用前面幾個部分來介紹這個模型以讓你掌握它的機制,稍后會介紹一下Twisted的特點。實際上,一開始,我們并不會使用Twisted,相反,會使用簡單的Python代碼來說明一個異步模型是如何工作的。我們在初次學習Twisted的時,會從你平常都不會直接使用的底層的實現講起。Twisted是一個高度抽象的體系,因此在使用它時,你會體會到其多層次性。但當你去學習尤其是嘗試著理解它是如何工作時,這種為抽像而帶來的多層次性會給你帶來極大的理解難度。所以,我們準備來個從內到外,從低層開始學習它。
模型
為了更好的理解異步編程模型的特點,我們來回顧一下兩個大家都熟悉的模型。在闡述過程中,我們假設一個包含三個相互獨立任務的程序。在此,除了規定這些任務都要完成自己工作外,我們先不作具體的解釋,后面我們會慢慢具體了解它們。請注意:在此我用"任務"這個詞,這意味著它需要完成一些事情。
第一個模型是單線程的同步模型,如圖1所示:
圖1 同步模型
這是最簡單的編程方式。在一個時刻,只能有一個任務在執行,并且前一個任務結束后一個任務才能開始。如果任務都能按照事先規定好的順序執行,最后一個任務的完成意味著前面所有的任務都已無任何差錯地完成并輸出其可用的結果—這是多么簡單的邏輯。 下面我們來呈現第二個模型,如圖2所示:
圖2 線程模型
在這個模型中,每個任務都在單獨的線程中完成。這些線程都是由操作系統來管理,若在多處理機、多核處理機的系統中可能會相互獨立的運行,若在單處理機上,則會交錯運行。關鍵點在于,在線程模式中,具體哪個任務執行由操作系統來處理。但編程人員則只需簡單地認為:它們的指令流是相互獨立且可以并行執行。雖然,從圖示看起來很簡單,實際上多線程編程是很麻煩的,你想啊,任務之間的要通信就要是線程之間的通信。線程間的通信那不是一般的復雜。什么郵槽、通道、共享內存。。。 唉—__-
一些程序用多處理機而不是多線程來實現并行運算。雖然具體的編程細節是不同的,但對于我們要研究的模型來說是一樣的。
下面我們來介紹一下異步編程模型,如圖3所示
圖3 異步模型
在這個模型中,任務是交錯完成,值得注意的是:這是在單線程的控制下。這要比多線程模型簡單多了,因為編程人員總可以認為只有一個任務在執行,而其它的在停止狀態。雖然在單處理機系統中,線程也是像圖3那樣交替進行。但作為程序員在使用多線程時,仍然需要使用圖2而不是圖3的來思考問題,以防止程序在挪到多處理機的系統上無法正常運行(考慮到兼容性)。但單線程的異步程序不管是在單處理機還是在多處理機上都能很好的運行。
在異步編程模型與多線程模型之間還有一個不同:在多線程程序中,對于停止某個線程啟動另外一個線程,其決定權并不在程序員手里而在操作系統那里,因此,程序員在編寫程序過程中必須要假設在任何時候一個線程都有可能被停止而啟動另外一個線程。相反,在異步模型中,一個任務要想運行必須顯式放棄當前運行的任務的控制權。這也是相比多線程模型來說,最簡潔的地方。 值得注意的是:將異步編程模型與同步模型混合在同一個系統中是可以的。但在介紹中的絕大多數時候,我們只研究在單個線程中的異步編程模型。
動機
我們已經看到異步編程模型之所以比多線程模型簡單在于其單指令流與顯式地放棄對任務的控制權而不是被操作系統隨機地停止。但是異步模型要比同步模型復雜得多。程序員必須將任務組織成序列來交替的小步完成。因此,若其中一個任務用到另外一個任務的輸出,則依賴的任務(即接收輸出的任務)需要被設計成為要接收系列比特或分片而不是一下全部接收。由于沒有實質上的并行,從我們的圖中可以看出,一個異步程序會花費一個同步程序所需要的時間,可能會由于異步程序的性能問題而花費更長的時間。
因此,就要問了,為什么還要使用異步模型呢? 在這兒,我們至少有兩個原因。首先,如果有一到兩個任務需要完成面向人的接口,如果交替執行這些任務,系統在保持對用戶響應的同時在后臺執行其它的任務。因此,雖然后臺的任務可能不會運行的更快,但這樣的系統可能會受歡迎的多。
然而,有一種情況下,異步模型的性能會高于同步模型,有時甚至會非常突出,即在比較短的時間內完成所有的任務。這種情況就是任務被強行等待或阻塞,如圖4所示:
圖4 同步模型中出現阻塞
在圖4中,灰色的部分代表這段時間某個任務被阻塞。為什么要阻塞一個任務呢?最直接的原因就是等待I/O的完成:傳輸數據或來自某個外部設備。一個典型的CPU處理數據的能力是硬盤或網絡的幾個數量級的倍數。因此,一個需要進行大I/O操作的同步程序需要花費大量的時間等待硬盤或網絡將數據準備好。正是由于這個原因,同步程序也被稱作為阻塞程序。
從圖4中可以看出,一個可阻塞的程序,看起來與圖3描述的異步程序有點像。這不是個巧合。異步程序背后的最主要的特點就在于,當出現一個任務像在同步程序一樣出現阻塞時,會讓其它可以執行的任務繼續執行,而不會像同步程序中那樣全部阻塞掉。因此一個異步程序只有在沒有任務可執行時才會出現"阻塞",這也是為什么異步程序被稱為非阻塞程序的原因。 任務之間的切換要不是此任務完成,要不就是它被阻塞。由于大量任務可能會被阻塞,異步程序等待的時間少于同步程序而將這些時間用于其它實時工作的處理(如與人打交道的接口),這樣一來,前者的性能必然要高很多。
與同步模型相比,異步模型的優勢在如下情況下會得到發揮:
有大量的任務,以至于可以認為在一個時刻至少有一個任務要運行
任務執行大量的I/O操作,這樣同步模型就會在因為任務阻塞而浪費大量的時間
任務之間相互獨立,以至于任務內部的交互很少。
這些條件大多在CS模式中的網絡比較繁忙的服務器端出現(如WEB服務器)。每個任務代表一個客戶端進行接收請求并回復的I/O操作。客戶的請求(相當于讀操作)都是相互獨立的。因此一個網絡服務是異步模型的典型代表,這也是為什么twisted是第一個也是最棒的網絡庫。
對你的假設
在展開討論前,我假設你已經有過用Python寫同步程序的經歷并且至少知道一點有關Python的Sockt編程的經驗。如果你從沒有寫過Socket程序,或許你可以去看看Socket模塊的文檔,尤其是后面的示例代碼。如果你沒有用過Python的話,那后面的描述對你來說可能比看周易還痛苦。
對你的環境假設
我一般是在Linux上使用Twisted,這個系列的示例代碼也是在Linux下完成的。首先聲明的是我并沒有故意讓代碼失去平臺無關性,但我所講述的一些內容確實可能僅僅適應于Linux和其它的類Unix(比如MAC OSX或FreeBSD)。WIndows是個奇怪詭異的地方(為什么這么評價Windows呢),如果你想嘗試在它上面學習這個系列,抱歉,如果出了問題,我無法提供任何幫助。 并且假設你已經安裝了Python和Twisted。我所提供的示例代碼是基于Python2.5和Twisted8.2.0。 你可以在單機上運行所有的示例代碼,也可以在網絡系統上運行它們。但是為了學習異步編程的機制,單機上學習是比較理想的。
獲取代碼的方法
使用git工具來獲取Dave的最新示例代碼。在shell或其它命令行上輸入以下命令(假設已經安裝git):
1
|
git clone git: //github .com /jdavisp3/twisted-intro .git |
下載結束后,解壓并進入第一層文件夾(你可以看到有個README文件)。
低效的詩歌服務器
雖然CPU的處理速度遠遠快于網絡,但網絡的處理速度仍然比人腦快,至少比人類的眼睛快。因此,想通過網絡來獲得CPU的視角是很困難的,尤其是在單機的回路模式中數據流全速傳輸時,更是困難重重。
我們所需要的是一個慢速低效詩歌服務器,其用人為的可變延時來體現對結果的影響。畢竟服務器要提供點東西嘛,我們就提供詩歌好了。目錄下面有個子目錄專門存放詩歌用的。
最簡單的慢速詩歌服務器在blocking-server/slowpoetry.py中實現。你可用下面的方式來運行它。
1
|
python blocking-server /slowpoetry .py poetry /ecstasy .txt |
上面這個命令將啟動一個阻塞的服務器,其提供"Ecstasy"這首詩。現在我們來看看它的源碼內容,正如你所見,這里面并沒有使用任何Twisted的內容,只是最基本的Socket編程操作。它每次只發送一定字節數量的內容,而每次中間延時一段時間。默認的是每隔0.1秒發送10個比特,你可以通過--delay和 --num-bytes參數來設置。例如每隔5秒發送50比特:
1
|
python blocking-server /slowpoetry .py --num-bytes 50 –-delay 5 poetry /ecstasy .txt |
當服務器啟動時,它會顯示其所監聽的端口號。默認情況下,端口號是在可用端口號池中隨機選擇的。你可能想使用固定的端口號,那么無需更改代碼,只需要在啟動命令中作下修改就OK了,如下所示:
1
|
python blocking-server /slowpoetry .py --port 10000 poetry /ecstasy .txt |
如果你裝有netcat工具,可以用如下命令來測試你的服務器(也可以用telnet):
1
|
netcat localhost 10000 |
如果你的服務器正常工作,那么你就可以看到詩歌在你的屏幕上慢慢的打印出來。對!你會注意到每次服務器都會發送過一行的內容過來。一旦詩歌傳送完畢,服務器就會關閉這條連接。
默認情況下,服務器只會監聽本地回環的端口。如果你想連接另外一臺機子的服務器,你可以指定其IP地址內容,命令行參數是 --iface選項。
不僅是服務器在發送詩歌的速度慢,而且讀代碼可以發現,服務器在服務一個客戶端時其它連接進來的客戶端只能處于等待狀態而得不到服務。這的確是一個低效慢速的服務器,要不是為了學習,估計沒有任何其它用處。
阻塞模式的客戶端
在示例代碼中有一個可以從多個服務器中順序(一個接一個)地下載詩歌的阻塞模式的客戶端。下面讓這個客戶端執行三個任務,正如第一個部分圖1描述的那樣。首先我們啟動三個服務器,提供三首不同的詩歌。在命令行中運行下面三條命令:
1
2
3
|
python blocking-server /slowpoetry .py --port 10000 poetry /ecstasy .txt --num-bytes 30 python blocking-server /slowpoetry .py --port 10001 poetry /fascination .txt python blocking-server /slowpoetry .py --port 10002 poetry /science .txt |
如果在你的系統中上面那些端口號有正在使用中,可以選擇其它沒有被使用的端口。注意,由于第一個服務器發送的詩歌是其它的三倍,這里我讓第一個服務器使用每次發送30個字節而不是默認的10個字節,這樣一來就以3倍于其它服務器的速度發送詩歌,因此它們會在幾乎相同的時間內完成工作。
現在我們使用阻塞模式的客戶端來獲取詩歌,運行如下所示的命令:
python blocking-client/get-poetry.py 10000 10001 10002
如果你修改了上面服務口器的端口,你需要在這里相應的修改以保持一致。由于這個客戶端采用的是阻塞模式,因此它會一首一首的下載,即只有在完成一首時才會開始下載另外一首。這個客戶端會像下面這樣打印出提示信息而不是將詩歌打印出來:
1
2
3
4
5
6
7
|
Task 1: get poetry from: 127.0.0.1:10000 Task 1: got 3003 bytes of poetry from 127.0.0.1:10000 in 0:00:10.126361 Task 2: get poetry from: 127.0.0.1:10001 Task 2: got 623 bytes of poetry from 127.0.0.1:10001 in 0:00:06.321777 Task 3: get poetry from: 127.0.0.1:10002 Task 3: got 653 bytes of poetry from 127.0.0.1:10002 in 0:00:06.617523 Got 3 poems in 0:00:23.065661 |
這圖1最典型的文字版了,每個任務下載一首詩歌。你運行后可能顯示的時間會與上面有所差別,并且也會隨著你改變服務器的發送時間參數而改變。嘗試著更改一下參數來觀測一下效果。
異步模式的客戶端
現在,我們來看看不用Twisted構建的異步模式的客戶端。首先,我們先運行它試試。啟動使用前面的三個端口來啟動三個服務器。如果前面開啟的還沒有關閉,那就繼續用它們好了。接下來,我們通過下面這段命令來啟動我們的異步模式的客戶端:
1
|
python async-client /get-poetry .py 10000 10001 10002 |
你或許會得到類似于下面的輸出:
1
2
3
4
5
6
7
8
9
10
|
Task 1: got 30 bytes of poetry from 127.0.0.1:10000 Task 2: got 10 bytes of poetry from 127.0.0.1:10001 Task 3: got 10 bytes of poetry from 127.0.0.1:10002 Task 1: got 30 bytes of poetry from 127.0.0.1:10000 Task 2: got 10 bytes of poetry from 127.0.0.1:10001 ... Task 1: 3003 bytes of poetry Task 2: 623 bytes of poetry Task 3: 653 bytes of poetry Got 3 poems in 0:00:10.133169 |
這次的輸出可能會比較長,這是由于在異步模式的客戶端中,每次接收到一段服務器發送來的數據都要打印一次提示信息,而服務器是將詩歌分成若干片段發送出去的。值得注意的是,這些任務相互交錯執行,正如第一部分圖3所示。
嘗試著修改服務器的設置(如將一個服務器的延時設置的長一點),來觀察一下異步模式的客戶端是如何針對變慢的服務器自動調節自身的下載來與較快的服務器保持一致。這正是異步模式在起作用。
還需要值得注意的是,根據上面的設置,異步模式的客戶端僅在10秒內完成工作,而同步模式的客戶端卻使用了23秒。現在回憶一下第一部分中圖3與圖4.通過減少阻塞時間,我們的異步模式的客戶端可以在更短的時間里完成下載。誠然,我們的異步客戶端也有些阻塞發生,那是由于服務器太慢了。由于異步模式的客戶端可以在不同的服務器來回切換,它比同步模式的客戶產生的阻塞就少得多。
更近一步的觀察
現在讓我們來讀一下異步模式客戶端的代碼。注意其與同步模式客戶端的差別:
- 異步模式客戶端一次性與全部服務器完成連接,而不像同步模式那樣一次只連接一個。
- 用來進行通信的Socket方法是非阻塞模的,這是通過調用setblocking(0)來實現的。
- select模塊中的select方法是用來識別其監視的socket是否有完成數據接收的,如果沒有它就處于阻塞狀態。
- 當從服務器中讀取數據時,會盡量多地從Socket讀取數據直到它阻塞為止,然后讀下一個Socket接收的數據(如果有數據接收的話)。這意味著我們需要跟蹤記錄從不同服務器傳送過來詩歌的接收情況(因為,一首詩的接收并不是連續完成,所以需要保證每個任務的可連續性,就得有冗余的信息來完成這一工作)。
異步模式中客戶端的核心就是最高層的循環體,即get_poetry函數。這個函數可以被拆分成兩個步驟:
- 使用select函數等待所有Socket,直到至少有一個socket有數據到來。
- 對每個有數據需要讀取的socket,從中讀取數據。但僅僅只是讀取有效數據,不能為了等待還沒來到的數據而發生阻塞。
- 重復前兩步,直到所有的socket被關閉。
可以看出,同步模式客戶端也有個循環體(在main函數內),但是這個循環體的每個迭代都是完成一首詩的下載工作。而在異步模式客戶端的每次迭代過程中,我們可以完成所有詩歌的下載或者是它們中的一些。我們并不知道在一個迭代過程中,在下載哪首詩,或者一次迭代中我們下載了多少數據。這些都依賴于服務器的發送速度與網絡環境。我們只需要select函數告訴我們哪個socket有數據需要接收,然后在保證不阻塞程序的前提下從其讀取盡量多的數據。
如果在服務器端口固定的條件下,同步模式的客戶端并不需要循環體,只需要順序羅列三個get_poetry就可以了。但是我們的異步模式的客戶端必須要有一個循環體來保證我們能夠同時監視所有的socket端。這樣我們就能在一次循環體中處理盡可能多的數據。
這個利用循環體來等待事件發生,然后處理發生的事件的模型非常常見,而被設計成為一個模式:reactor模式。其圖形化表示如圖5所示:
這個循環就是個"reactor"(反應堆),因為它等待事件的發生然后對其作相應的反應。正因為如此,它也被稱作事件循環。由于交互式系統都要進行I/O操作,因此這種循環也有時被稱作select loop,這是由于select調用被用來等待I/O操作。因此,在本程序中的select循環中,一個事件的發生意味著一個socket端處有數據來到。值得注意的是,select并不是唯一的等待I/O操作的函數,它僅僅是一個比較古老的函數而已(因此才被用的如此廣泛)。現在有一些新API可以完成select的工作而且性能更優,它們已經在不同的系統上實現了。不考慮性能上的因素,它們都完成同樣的工作:監視一系列sockets(文件描述符)并阻塞程序,直到至少有一個準備好的I/O操作。
嚴格意義上來說,我們的異步模式客戶端中的循環并不是reactor模式,因為這個循環體并沒有獨立于業務處理(在此是接收具體的服務器傳送來的詩歌)之外。它們被混合在一起。一個真正reactor模式的實現是需要實現循環獨立抽象出來并具有如下的功能:
- 監視一系列與你I/O操作相關的文件描述符(description)
- 不停地向你匯報那些準備好的I/O操作的文件描述符
一個設計優秀的reactor模式實現需要做到:
- 處理所有不同系統會出現的I/O事件
- 提供優雅的抽象來幫助你在使用reactor時少花些心思去考慮它的存在
- 提供你可以在抽象層外使用的公共協議實現。
- 好了,我們上面所說的其實就是Twisted — 健壯、跨平臺實現了reactor模式并含有很多附加功能。