前言
上一篇文章《伺服器端網路程式設計之 IO 模型》中講到伺服器端高效能網路程式設計的核心在於架構,而架構的核心在於程式/執行緒模型的選擇。本文將主要介紹傳統的和目前流行的程式/執行緒模型,在講程式/執行緒模型之前需要先介紹一種設計模式: Reactor 模式,不明白的看這裡《設計模式詳解》,文中有一句話對 Reactor 模式總結的很好,引用下。
Reactor 模式首先是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers;這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler。如果用圖表示的如下:
不知道讀者有沒有發現 Reactor 模式跟 IO 模型中的 IO 多路複用模型非常相似 ,在學習網路程式設計過程中也被這兩個概念迷惑了很久。其實在設計模式層面 IO 多路複用也是採用 Reactor 模式的。IO 多路複用模型可以看成是 Reactor 模式在 IO 模型上的應用,而今天我們要講的是 Reactor 模式在程式/執行緒模型上的應用。
在我的看來,程式/執行緒模型可以分為非 Reactor 模式和 Reactor 模式兩種(當然還有 Proactor 模式,這種本文先不講,因為使用的比較少,而且我也還沒搞懂這種模式)。非 Reactor 模式和 Reactor 模式的兩種程式/執行緒模型下具體又分很多種,後面會一一列舉。非 Reactor 模式的程式/執行緒模型是傳統的模型,現在已經很少見,放在這裡主要是讓讀者做個瞭解同時與 Reactor 模式的程式/執行緒模式做個對比。
非 Reactor 模式的程式/執行緒模型
傳統模型不使用 IO 多路使用,所以問題比較多。初學者建議看下這部分,如果你覺得被迷糊了或者不感興趣可以跳過該部分,直接看 Reactor 模式的部分即可。但該部分的第 1、2 點需要了解下。
1、單程式單執行緒
優點:程式碼簡單,無需去了解程式、執行緒的概念,適合學習網路程式設計的初學者。在不瞭解程式/執行緒模型情況下的預設模型。
缺點:沒有任何實用價值。
2、單程式多執行緒
描述:程式只做建立連線的動作,每接收一個連線就建立一個執行緒,在此連線上的讀->業務處理->寫->關閉連線都線上程中去做,可以採用執行緒池的方式減少執行緒的建立和銷燬。這種執行緒模型有一定的應用場景,Tomcat 三種執行緒模型之 BIO 用的就是這種程式/執行緒模型。初學者在學習完單程式單執行緒模型後對執行緒有所瞭解即可開始學習該種模型,可以實現一個簡單的聊天室程式。優點:可以同時與多個 Client 建立連線,接收連線和處理連線業務分開。
缺點:每個連線佔用一個執行緒,當連線上沒有資料的時候造成執行緒資源浪費,可以建立的連線數比較有限。
3、多程式單執行緒
描述:
(1) 主程式啟動時建立監聽套接字並監聽,然後 fork 出 N 個子程式。
(2) 由於父子程式的繼承性,子程式同時也在埠監聽,然後在父程式中關閉監聽。
(3) 父程式負責子程式的建立、銷燬、資源回收等,子程式負責連線的建立->Read->業務處理->Write 等。
由於所有程式都在同一個埠監聽,該模型會出現一個比較知名的現象---驚群現象:當有一個連線來臨時,所有子程式都會被喚醒,但是最後能與 Client 建立連線的只有一個,造成資源浪費(系通排程也是消耗 CPU 的)。不過 linux 2.6 版本以後已經在核心消除了驚群,當有連線來臨只會喚醒一個等待在 accept() 上的程式。即使核心沒修復,在應用層也可以用鎖的方式防止驚群。
缺點:這種模型是單程式單執行緒的進化版本,然而並沒有什麼卵用。且增加了開發的難度。所以不列出它的優點,介紹這種模型主要是引出驚群的概念,在後面的 Reactor 模型中的多程式情況下也會出現類似的情況。
Reactor 模式的程式/執行緒模型
該模式一般是 Reactor 模型 + IO 多路複用,下面的任何一種模型都具有一定的實用場景。
1、單程式單執行緒
描述:只有一個程式,監聽套接字和連線套接字上的事件都由 Select 來處理,
(1) 如果有建立連線的請求過來,Acceptor 負責接受並與之建立連線,同時將連線套接字加入 Select 進行監聽;
(2) 如果某個連線上有讀事件則進行 Read->業務處理->Write 等操作;
(3) 如此迴圈反覆。
優點:程式設計簡單,對於業務處理不復雜的後臺,基本能滿足伺服器端網路程式設計。老東家的伺服器端程式全是這種模式,主要原因有如下
(1) 如果一臺機器效能不行,那就向叢集中新增一臺。
(2) 業務處理並不複雜。
(3) 擴充套件成多程式的話,如果不是多核意義不大。
(4) 如果採用單程式多執行緒,C++ 處理執行緒不像 Java 簡單,還要考慮併發的問題,收益比不大。
缺點:會有阻塞,在進行業務處理的時候不能進行其他操作:如建立連線,讀取其他套接字上的資料等。
2、單程式多執行緒
描述:與單程式單執行緒類似,不同的是該模型將業務處理放線上程中,程式就不會阻塞在業務處理上。優點:比較完美的程式/執行緒模型,在 Java 實現中複雜度也不高。很多網路庫都是基於此,比如 Netty 。
缺點:待補充。
3、多程式單執行緒:
描述:與非 Reactor 模式中的多程式單執行緒相似,只是本模式在子程式中使用了 IO 多路複用,實用性以下就上來了。大名鼎鼎的 nginx 就採用這種程式/執行緒模型優點:程式設計相對簡單,充分利用多核。能滿足高併發,不然 nginx 也不可能採用這種模式。
缺點:子程式還是會阻塞在業務處理上。
4、多程式多執行緒
描述:這裡不再畫出圖形,就是在在子程式上將業務處理交給多執行緒處理,參考單程式多執行緒裡的執行緒池那裡。
優點:充分利用多核同時子程式不會阻塞在業務處理上
缺點:程式設計複雜。
5、主從程式 +多執行緒:
描述:前面幾種 Reactor 模式的程式/執行緒模型中,連線的建立和連線的讀寫都是在同一程式中。本模型中將連線的建立和連線讀寫放在不同的程式中。(1) 主程式在監聽套接字上 Select 阻塞,一旦有請求過來則與之建立連線,並將連線套接字傳遞給從程式。
(2) 從程式在連線套接字上 Select 阻塞,一旦連線上有資料過來則進行 Read,並將業務處理通過執行緒來處理。如果有必要還會向連線 Write 資料。
優點:連線的建立和連線的讀寫分開在不同程式中,處理效率會更高。該模型比單程式多執行緒模式還更優一點,且也可以利用多核。
缺點:程式設計複雜。
總結
以上就是常見的程式/執行緒模型,使用了 IO 多路複用的執行緒模型一般都可以稱為 Reactor 模型,所以不用糾結 IO 多路複用與 Reactor 模式之間的關係。由 C++ 轉 Java 後,雖然不再從事網路程式設計。但是在看完《Netty 權威指南》後又想結合之前的工作經驗講講一個網路庫設計需要考慮的一些要素,同時做一些 Netty 的分享。在後續的文章中會出,敬請期待!