[Twisted] Part 1: In Which We Begin at the Beginning

Preface

最近有人在Twisted mailing list提出像「為任務緊急的人提供一份Twisted介紹」的這種需求。坦白說,這裡的文章並沒有辦法快速的介紹,尤其對於Twisted框架和基於Python 的非同步程式設計而言,可能短時間無法講清楚。因此,如果你時間緊急,這裡恐怕不是你想找的資料。

我相信如果對非同步程式設計模型一無所知,快速的介紹同樣無法讓你對其有所理解,至少你得稍微懂點基礎知識吧。我已經用Twisted框架幾年了,因此思考過我當初是怎麼學習它(學得很慢)並發現學習它的最大難度並不在Twisted本身,而在於對其模型的理解,只有理解了這個模型,你才能更好去撰寫和理解非同步程式的程式。以多數免費軟體的標準來看,大部分Twisted的代碼寫得很清晰,其線上文件也非常好。但是沒有建立良好的心智模型,不管是讀Twisted codebase、用Twisted寫程式、或者閱讀大部分的文件,你都會感到非常的傷腦筋。

所以這份介紹的第一部分是來幫助你建立心智模型,之後我們才會介紹Twisted的功能。實際上,一開始,我們並不會使用Twisted,相反,會使用簡單的Python來說明一個非同步模型是如何工作的。我們在初次學習Twisted的時,會從你平常都不會直接使用的底層的方面講起。Twisted是一個高度抽象的系統,因此在使用它時,你會體會到其巨大的影響力。但當你開始學習Twisted,尤其在嘗試著理解它是如何工作時,許多抽象的層次可能會造成麻煩。所以,我們準備來個從內到外,從底層開始學習它。

一旦你有了基礎知識,我相信你會發現閱讀Twisted文件瀏覽原始碼會變得更簡單,讓我們開始吧!

The Models

為了與非同步程式設計模型對比,我們來回顧一下兩個大家都熟悉的模型。在闡述過程中,我們假設一個程式,它由三個概念上不同的任務(taks)所組成,這些任務必須都完成後程式才算完成。在此,除了規定這些任務都要完成自己工作外,我們先不作具體的解釋,後面我們會慢慢具體瞭解它們。請注意:在此我用「任務」這個詞,這意味著它需要完成一些事情。

第一個模型是單執行緒的同步模型,如圖1所示:
1.the%2Bsynchronous%2Bmodel.png-Part 1: In Which We Begin at the Beginning

圖1.同步模型


這是最簡單的程式設計方式。在一個時間點內,只能有一個任務在執行,並且前一個任務結束後另一個任務才能開始。如果任務都能按照事先規定好的順序執行,最後一個任務完成就表示前面所有的任務都已沒有任何差錯地完成,並輸出其可用的結果—這是相當簡單的邏輯。

下面我們來展示第二個模型,如圖2所示:
2.the%2Bthreaded%2Bmodel.png-Part 1: In Which We Begin at the Beginning

圖2.執行緒模型


在這個模型中,每個任務都在單獨的執行緒中完成。這些執行緒都是由作業系統來管理,若在多處理器(multiple processors)或多核心(multiple cores)的系統中可能會相互獨立的運行,若在單處理機上,則會交錯運行。關鍵點在於,在執行緒模式中,執行的細節由作業系統來處理,程式設計人員則只需簡單地認為:它們的指令流(instruction streams)是相互獨立且可以並存執行。雖然,從圖示看起來很簡單,實際上多執行緒程式設計是很麻煩的,因為執行緒彼此之間需要互相通訊與協調,這是一個進階的程式設計主題,而且可能很難得到正確的結果。

一些程式用多程序(multiple processes)而不是多執行緒(multiple threads)來實現並行運算。雖然具體的程式撰寫的細節不同,但對於我們要研究的模型來說是一樣的。

下面我們來介紹一下非同步程式設計模型,如圖3所示:
3.the%2Basynchronous%2Bmodel.png-Part 1: In Which We Begin at the Beginning

圖3.非同步模型


在這個模型中,任務是交錯完成,但是是在單一個執行緒的控制下。這要比多執行緒模型簡單多了,因為程式設計人員可以認為只有一個任務在執行,而其它的在停止狀態。雖然在單處理機系統中,執行緒也是像圖3那樣交替進行。但作為程式師在使用多執行緒時,仍然需要使用圖2而不是圖3的來思考問題,以防止程式挪到多處理器的系統上時無法正常運行。單執行緒的非同步系統始終以交錯的方式再執行,即使在多處理器系統上也是如此。

在非同步模型與執行緒模型之間還有一個不同:在執行緒程式中,對於停止某個執行緒並啟動另外一個執行緒,其決定權並不在程式師手裡而在作業系統那裡。因此,程式師在編寫程式過程中必須要假設,在任何時候執行緒都有可能被停止並被替換。相反,在非同步模型中,任務會一直執行到它明確的放棄了控制權交給其他的任務。這也是相比多執行緒模型來說,最簡潔的地方。

注意,將非同步與同步模型混合在同一個系統中是可以的。但在這份介紹中的絕大部分,我們只研究在單一執行緒中的非同步模型。

The Motivation

我們已經看到非同步模型之所以比執行緒模型簡單,在於其單指令流(single instruction stream)與明確的放棄對任務的控制權,而不是被作業系統隨機地停止。但是非同步模型要比同步模型複雜得多。程式設計師必須將每個任務組織成一套間歇執行小步驟。因此,若其中一個任務用到另外一個任務的輸出,則依賴的任務(譯註: 即接收其他任務輸出的任務)需要被設計成為能接收一組零碎的輸出,而不是一次完整的輸出。由於沒有實際的並行性,從我們的圖中可以看出,一個非同步程式會花費一個同步程式所需要的時間,也可能更長,因為非同步程式可能會有比較差的locality of reference (存取局部性)

因此為什麼還要使用非同步模型呢?我們至少有兩個原因。首先,如果有一到多個任務負責為人類實現介面,如果交替執行這些任務,系統在保持回應使用者的操作時,同時可以在背景執行其它的任務。因此,雖然背景的任務可能不會運行的更快,但這樣的系統可能會受歡迎的多。

然而,有一種情況下,非同步模型的性能會高於同步模型,有時甚至會非常突出,即在比較短的時間內完成所有的任務。這種情況就是任務被強迫等待或阻塞,如圖4所示:
4.blocking%2Bin%2Ba%2Bsynchronous%2Bprogram.png-Part 1: In Which We Begin at the Beginning

圖4.在同步程式中的阻塞


在圖4中,灰色的部分代表這段時間某個任務被阻塞。為什麼要阻塞一個任務呢?最常見的原因就是等待傳輸資料或來自某個外部設備的I/O完成。一個典型的CPU所能處理的資料傳輸速度,是比硬碟或網路連結能夠維持的速度更快好幾個數量級(order of magnitude)。因此,一個需要進行大量I/O操作的同步程式需要花費大量的時間等待硬碟或網路將資料準備好。正是由於這個原因,同步程式也被稱作為阻塞程式(blocking program)。

從圖4中可以看出,一個阻塞程式看起來與圖3描述的非同步程式有點像。這不是個巧合。非同步程式背後的最主要的特點就在於,當任務出現像在同步程式一樣的阻塞時,會讓其它可以執行的任務繼續執行,而不會像同步程式中那樣全部阻塞掉。因此一個非同步程式只有在沒有任務可執行時才會出現「阻塞」,這也是為什麼被稱為非阻塞程式(non-blocking program)的原因。

任務之間的切換可能是此任務完成,或者是它被阻塞。由於大量任務可能會被阻塞,非同步程式使用比同步程式少的整體等待時間,將這些時間用於處理其它的即時工作。

與同步模型相比,非同步模型的優勢在如下情況下會得到發揮:

  1. 有大量的任務,因此在一個時間點至少有一個任務要運行。
  2. 任務執行大量的I/O操作,這樣同步模型就會在因為任務阻塞而浪費大量的時間。
  3. 任務之間相互獨立,以至於任務彼此間內部交互很少。

這些條件幾乎完美的代表著client-server環境中的繁忙網路伺服器(如WEB伺服器),每個任務以接收請求並傳送回復的的形式代表一個用戶端進行I/O請求。客戶的請求(主要是讀取)都是相互獨立的。因此一個網路服務是非同步模型的典型代表,這也是為什麼Twisted是個重要的網路函式庫。

Onward and Upward

這是Part 1的結束。在Part 2中,我們將寫一些盡可能簡單的(不使用Twisted)阻塞和非阻塞的網路程式,來了解非同步Python程式實際工作的方式。

留言