伺服器模型——從單執行緒阻塞到多執行緒非阻塞(中)

超人汪小建發表於2019-02-27

前言的前言

伺服器模型涉及到執行緒模式和IO模式,搞清楚這些就能針對各種場景有的放矢。該系列分成三部分:

  • 單執行緒/多執行緒阻塞I/O模型
  • 單執行緒非阻塞I/O模型
  • 多執行緒非阻塞I/O模型,Reactor及其改進

前言

這裡探討的伺服器模型主要指的是伺服器端對I/O的處理模型。從不同維度可以有不同的分類,這裡從I/O的阻塞與非阻塞、I/O處理的單執行緒與多執行緒角度探討伺服器模型。

對於I/O,可以分成阻塞I/O與非阻塞I/O兩大型別。阻塞I/O在做I/O讀寫操作時會使當前執行緒進入阻塞狀態,而非阻塞I/O則不進入阻塞狀態。

對於執行緒,單執行緒情況下由一條執行緒負責所有客戶端連線的I/O操作,而多執行緒情況下則由若干執行緒共同處理所有客戶端連線的I/O操作。

單執行緒非阻塞I/O模型

多執行緒阻塞I/O模型通過引入多執行緒確實提高了伺服器端的併發處理能力,但每個連線都需要一個執行緒負責I/O操作。當連線數量較多時可能導致機器執行緒數量太多,而這些執行緒大多數時間卻處於等待狀態,造成極大的資源浪費。鑑於多執行緒阻塞I/O模型的缺點,有沒有可能用一個執行緒就可以維護多個客戶端連線並且不會阻塞在讀寫操作呢?下面介紹單執行緒非阻塞I/O模型。

單執行緒非阻塞I/O模型最重要的一個特點是,在呼叫讀取或寫入介面後立即返回,而不會進入阻塞狀態。在探討單執行緒非阻塞I/O模型前必須要先了解非阻塞情況下套接字事件的檢測機制,因為對於單執行緒非阻塞模型最重要的事情是檢測哪些連線有感興趣的事件發生。一般會有如下三種檢測方式。

應用程式遍歷套接字的事件檢測

當多個客戶端向伺服器請求時,伺服器端會儲存一個套接字連線列表中,應用層執行緒對套接字列表輪詢嘗試讀取或寫入。對於讀取操作,如果成功讀取到若干資料,則對讀取到的資料進行處理;如果讀取失敗,則下一個迴圈再繼續嘗試。對於寫入操作,先嚐試將資料寫入指定的某個套接字,寫入失敗則下一個迴圈再繼續嘗試。

這裡寫圖片描述

這樣看來,不管有多少個套接字連線,它們都可以被一個執行緒管理,一個執行緒負責遍歷這些套接字列表,不斷地嘗試讀取或寫入資料。這很好地利用了阻塞的時間,處理能力得到提升。但這種模型需要在應用程式中遍歷所有的套接字列表,同時需要處理資料的拼接,連線空閒時可能也會佔用較多CPU資源,不適合實際使用。對此改進的方法是使用事件驅動的非阻塞方式。

核心遍歷套接字的事件檢測

這種方式將套接字的遍歷工作交給了作業系統核心,把對套接字遍歷的結果組織成一系列的事件列表並返回應用層處理。對於應用層,它們需要處理的物件就是這些事件,這就是其中一種事件驅動的非阻塞方式的實現。

伺服器端有多個客戶端連線,應用層向核心請求讀寫事件列表。核心遍歷所有套接字並生成對應的可讀列表readList和可寫列表writeList。readList標明瞭每個套接字是否可讀,例如套接字1的值為1,表示可讀,socket2的值為0,表示不可讀。writeList則標明瞭每個套接字是否可寫。應用層遍歷讀寫事件列表readList和writeList,做相應的讀寫操作。

這裡寫圖片描述

核心遍歷套接字時已經不用在應用層對所有套接字進行遍歷,將遍歷工作下移到核心層,這種方式有助於提高檢測效率。然而,它需要將所有連線的可讀事件列表和可寫事件列表傳到應用層,假如套接字連線數量變大,列表從核心複製到應用層也是不小的開銷。另外,當活躍連線較少時,核心與應用層之間存在很多無效的資料副本,因為它將活躍和不活躍的連線狀態都複製到應用層中。

核心基於回撥的事件檢測

通過遍歷的方式檢測套接字是否可讀可寫是一種效率比較低的方式,不管是在應用層中遍歷還是在核心中遍歷。所以需要另外一種機制來優化遍歷的方式,那就是回撥函式。核心中的套接字都對應一個回撥函式,當客戶端往套接字傳送資料時,核心從網路卡接收資料後就會呼叫回撥函式,在回撥函式中維護事件列表,應用層獲取此事件列表即可得到所有感興趣的事件。

核心基於回撥的事件檢測方式有兩種。第一種是用可讀列表readList和可寫列表writeList標記讀寫事件,套接字的數量與readList和writeList兩個列表的長度一樣,readList第一個元素標為1則表示套接字1可讀,同理,writeList第二個元素標為1則表示套接字2可寫。如圖所示,多個客戶端連線伺服器端,當客戶端傳送資料過來時,核心從網路卡複製資料成功後呼叫回撥函式將readList第一個元素置為1,應用層傳送請求讀、寫事件列表,返回核心包含了事件標識的readList和writeList事件列表,進而分表遍歷讀事件列表readList和寫事件列表writeList,對置為1的元素對應的套接字進行讀或寫操作。這樣就避免了遍歷套接字的操作,但仍然有大量無用的資料(狀態為0的元素)從核心複製到應用層中。於是就有了第二種事件檢測方式。

這裡寫圖片描述

核心基於回撥的事件檢測方式二如圖所示。伺服器端有多個客戶端套接字連線。首先,應用層告訴核心每個套接字感興趣的事件。接著,當客戶端傳送資料過來時,對應會有一個回撥函式,核心從網路卡複製資料成功後即調回撥函式將套接字1作為可讀事件event1加入到事件列表。同樣地,核心發現網路卡可寫時就將套接字2作為可寫事件event2新增到事件列表中。最後,應用層向核心請求讀、寫事件列表,核心將包含了event1和event2的事件列表返回應用層,應用層通過遍歷事件列表得知套接字1有資料待讀取,於是進行讀操作,而套接字2則可以寫入資料。

這裡寫圖片描述

上面兩種方式由作業系統核心維護客戶端的所有連線並通過回撥函式不斷更新事件列表,而應用層執行緒只要遍歷這些事件列表即可知道可讀取或可寫入的連線,進而對這些連線進行讀寫操作,極大提高了檢測效率,自然處理能力也更強。

對於Java來說,非阻塞I/O的實現完全是基於作業系統核心的非阻塞I/O,它將作業系統的非阻塞I/O的差異遮蔽並提供統一的API,讓我們不必關心作業系統。JDK會幫我們選擇非阻塞I/O的實現方式,例如對於Linux系統,在支援epoll的情況下JDK會優先選擇用epoll實現Java的非阻塞I/O。這種非阻塞方式的事件檢測機制就是效率最高的“核心基於回撥的事件檢測”中的第二種方式。

在瞭解了非阻塞模式下的事件檢測方式後,重新回到對單執行緒非阻塞I/O模型的討論。雖然只有一個執行緒,但是它通過把非阻塞讀寫操作與上面幾種檢測機制配合就可以實現對多個連線的及時處理,而不會因為某個連線的阻塞操作導致其他連線無法處理。在客戶端連線大多數都保持活躍的情況下,這個執行緒會一直迴圈處理這些連線,它很好地利用了阻塞的時間,大大提高了這個執行緒的執行效率。

單執行緒非阻塞I/O模型的主要優勢體現在對多個連線的管理,一般在同時需要處理多個連線的發場景中會使用非阻塞NIO模式,此模型下只通過一個執行緒去維護和處理連線,這樣大大提高了機器的效率。一般伺服器端才會使用NIO模式,而對於客戶端,出於方便及習慣,可使用阻塞模式的套接字進行通訊。

=============廣告時間===============

公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

歡迎關注:

這裡寫圖片描述

相關文章