前言的前言
服務(wù)器模型涉及到線程模式和IO模式,搞清楚這些就能針對(duì)各種場(chǎng)景有的放矢。該系列分成三部分:
單線程/多線程阻塞I/O模型
單線程非阻塞I/O模型
多線程非阻塞I/O模型,Reactor及其改進(jìn)
前言
這里探討的服務(wù)器模型主要指的是服務(wù)器端對(duì)I/O的處理模型。從不同維度可以有不同的分類,這里從I/O的阻塞與非阻塞、I/O處理的單線程與多線程角度探討服務(wù)器模型。
對(duì)于I/O,可以分成阻塞I/O與非阻塞I/O兩大類型。阻塞I/O在做I/O讀寫操作時(shí)會(huì)使當(dāng)前線程進(jìn)入阻塞狀態(tài),而非阻塞I/O則不進(jìn)入阻塞狀態(tài)。
對(duì)于線程,單線程情況下由一條線程負(fù)責(zé)所有客戶端連接的I/O操作,而多線程情況下則由若干線程共同處理所有客戶端連接的I/O操作。
單線程阻塞I/O模型
單線程阻塞I/O模型是最簡(jiǎn)單的一種服務(wù)器模型,幾乎所有程序員在剛開始接觸網(wǎng)絡(luò)編程時(shí)都從這個(gè)簡(jiǎn)單的模型開始。這種模型只能同時(shí)處理一個(gè)客戶端訪問,并且在I/O操作上是阻塞的,線程會(huì)一直在等待,而不會(huì)做其他事情。對(duì)于多個(gè)客戶端訪問,必須要等到前一個(gè)客戶端訪問結(jié)束才能進(jìn)行下一個(gè)訪問的處理,請(qǐng)求一個(gè)一個(gè)排隊(duì),只提供一問一答服務(wù)。
首先,服務(wù)器必須初始化一個(gè)套接字服務(wù)器,并綁定某個(gè)端口號(hào)并使之監(jiān)聽客戶端的訪問。接著,客戶端1調(diào)用服務(wù)器的服務(wù),服務(wù)器接收到請(qǐng)求后對(duì)其進(jìn)行處理,處理完后寫數(shù)據(jù)回客戶端1,整個(gè)過程都是在一個(gè)線程里面完成的。最后,處理客戶端2的請(qǐng)求并寫數(shù)據(jù)回客戶端2,期間就算客戶端2在服務(wù)器處理完客戶端1之前就進(jìn)行請(qǐng)求,也要等服務(wù)器對(duì)客戶端1響應(yīng)完后才會(huì)對(duì)客戶端2進(jìn)行響應(yīng)處理。
這種模型的特點(diǎn)在于單線程和阻塞I/O。單線程即服務(wù)器端只有一個(gè)線程處理客戶端的所有請(qǐng)求,客戶端連接與服務(wù)器端的處理線程比是n:1,它無法同時(shí)處理多個(gè)連接,只能串行處理連接。而阻塞I/O是指服務(wù)器在讀寫數(shù)據(jù)時(shí)是阻塞的,讀取客戶端數(shù)據(jù)時(shí)要等待客戶端發(fā)送數(shù)據(jù)并且把操作系統(tǒng)內(nèi)核復(fù)制到用戶進(jìn)程中,這時(shí)才解除阻塞狀態(tài)。寫數(shù)據(jù)回客戶端時(shí)要等待用戶進(jìn)程將數(shù)據(jù)寫入內(nèi)核并發(fā)送到客戶端后才解除阻塞狀態(tài)。這種阻塞給網(wǎng)絡(luò)編程帶來了一個(gè)問題,服務(wù)器必須要等到客戶端成功接收才能繼續(xù)往下處理另外一個(gè)客戶端的請(qǐng)求,在此期間線程將無法響應(yīng)任何客戶端請(qǐng)求。
該模型的特點(diǎn):它是最簡(jiǎn)單的服務(wù)器模型,整個(gè)運(yùn)行過程都只有一個(gè)線程,只能支持同時(shí)處理一個(gè)客戶端的請(qǐng)求(如果有多個(gè)客戶端訪問,就必須排隊(duì)等待),服務(wù)器系統(tǒng)資源消耗較小,但并發(fā)能力低,容錯(cuò)能力差。
多線程阻塞I/O模型
針對(duì)單線程阻塞I/O模型的缺點(diǎn),我們可以使用多線程對(duì)其進(jìn)行改進(jìn),使之能并發(fā)地對(duì)多個(gè)客戶端同時(shí)進(jìn)行響應(yīng)。多線程模型的核心就是利用多線程機(jī)制為每個(gè)客戶端分配一個(gè)線程。服務(wù)器端開始監(jiān)聽客戶端的訪問,假如有兩個(gè)客戶端發(fā)送請(qǐng)求過來,服務(wù)器端在接收到客戶端請(qǐng)求后分別創(chuàng)建兩個(gè)線程對(duì)它們進(jìn)行處理,每條線程負(fù)責(zé)一個(gè)客戶端連接,直到響應(yīng)完成。期間兩個(gè)線程并發(fā)地為各自對(duì)應(yīng)的客戶端處理請(qǐng)求,包括讀取客戶端數(shù)據(jù)、處理客戶端數(shù)據(jù)、寫數(shù)據(jù)回客戶端等操作。
這種模型的I/O操作也是阻塞的,因?yàn)槊總€(gè)線程執(zhí)行到讀取或?qū)懭氩僮鲿r(shí)都將進(jìn)入阻塞狀態(tài),直到讀取到客戶端的數(shù)據(jù)或數(shù)據(jù)成功寫入客戶端后才解除阻塞狀態(tài)。盡管I/O操作阻塞,但這種模式比單線程處理的性能明顯高了,它不用等到第一個(gè)請(qǐng)求處理完才處理第二個(gè),而是并發(fā)地處理客戶端請(qǐng)求,客戶端連接與服務(wù)器端處理線程的比例是1:1。
多線程阻塞I/O模型的特點(diǎn):支持對(duì)多個(gè)客戶端并發(fā)響應(yīng),處理能力得到大幅提高,有較大的并發(fā)量,但服務(wù)器系統(tǒng)資源消耗量較大,而且多線程之間會(huì)產(chǎn)生線程切換成本,同時(shí)擁有較復(fù)雜的結(jié)構(gòu)。
單線程非阻塞I/O模型
多線程阻塞I/O模型通過引入多線程確實(shí)提高了服務(wù)器端的并發(fā)處理能力,但每個(gè)連接都需要一個(gè)線程負(fù)責(zé)I/O操作。當(dāng)連接數(shù)量較多時(shí)可能導(dǎo)致機(jī)器線程數(shù)量太多,而這些線程大多數(shù)時(shí)間卻處于等待狀態(tài),造成極大的資源浪費(fèi)。鑒于多線程阻塞I/O模型的缺點(diǎn),有沒有可能用一個(gè)線程就可以維護(hù)多個(gè)客戶端連接并且不會(huì)阻塞在讀寫操作呢?下面介紹單線程非阻塞I/O模型。
單線程非阻塞I/O模型最重要的一個(gè)特點(diǎn)是,在調(diào)用讀取或?qū)懭?a target="_blank">接口后立即返回,而不會(huì)進(jìn)入阻塞狀態(tài)。在探討單線程非阻塞I/O模型前必須要先了解非阻塞情況下套接字事件的檢測(cè)機(jī)制,因?yàn)閷?duì)于單線程非阻塞模型最重要的事情是檢測(cè)哪些連接有感興趣的事件發(fā)生。一般會(huì)有如下三種檢測(cè)方式。
應(yīng)用程序遍歷套接字的事件檢測(cè)
當(dāng)多個(gè)客戶端向服務(wù)器請(qǐng)求時(shí),服務(wù)器端會(huì)保存一個(gè)套接字連接列表中,應(yīng)用層線程對(duì)套接字列表輪詢嘗試讀取或?qū)懭?。?duì)于讀取操作,如果成功讀取到若干數(shù)據(jù),則對(duì)讀取到的數(shù)據(jù)進(jìn)行處理;如果讀取失敗,則下一個(gè)循環(huán)再繼續(xù)嘗試。對(duì)于寫入操作,先嘗試將數(shù)據(jù)寫入指定的某個(gè)套接字,寫入失敗則下一個(gè)循環(huán)再繼續(xù)嘗試。
這樣看來,不管有多少個(gè)套接字連接,它們都可以被一個(gè)線程管理,一個(gè)線程負(fù)責(zé)遍歷這些套接字列表,不斷地嘗試讀取或?qū)懭霐?shù)據(jù)。這很好地利用了阻塞的時(shí)間,處理能力得到提升。但這種模型需要在應(yīng)用程序中遍歷所有的套接字列表,同時(shí)需要處理數(shù)據(jù)的拼接,連接空閑時(shí)可能也會(huì)占用較多CPU資源,不適合實(shí)際使用。對(duì)此改進(jìn)的方法是使用事件驅(qū)動(dòng)的非阻塞方式。
內(nèi)核遍歷套接字的事件檢測(cè)
這種方式將套接字的遍歷工作交給了操作系統(tǒng)內(nèi)核,把對(duì)套接字遍歷的結(jié)果組織成一系列的事件列表并返回應(yīng)用層處理。對(duì)于應(yīng)用層,它們需要處理的對(duì)象就是這些事件,這就是其中一種事件驅(qū)動(dòng)的非阻塞方式的實(shí)現(xiàn)。
服務(wù)器端有多個(gè)客戶端連接,應(yīng)用層向內(nèi)核請(qǐng)求讀寫事件列表。內(nèi)核遍歷所有套接字并生成對(duì)應(yīng)的可讀列表readList和可寫列表writeList。readList標(biāo)明了每個(gè)套接字是否可讀,例如套接字1的值為1,表示可讀,socket2的值為0,表示不可讀。writeList則標(biāo)明了每個(gè)套接字是否可寫。應(yīng)用層遍歷讀寫事件列表readList和writeList,做相應(yīng)的讀寫操作。
內(nèi)核遍歷套接字時(shí)已經(jīng)不用在應(yīng)用層對(duì)所有套接字進(jìn)行遍歷,將遍歷工作下移到內(nèi)核層,這種方式有助于提高檢測(cè)效率。然而,它需要將所有連接的可讀事件列表和可寫事件列表傳到應(yīng)用層,假如套接字連接數(shù)量變大,列表從內(nèi)核復(fù)制到應(yīng)用層也是不小的開銷。另外,當(dāng)活躍連接較少時(shí),內(nèi)核與應(yīng)用層之間存在很多無效的數(shù)據(jù)副本,因?yàn)樗鼘⒒钴S和不活躍的連接狀態(tài)都復(fù)制到應(yīng)用層中。
內(nèi)核基于回調(diào)的事件檢測(cè)
通過遍歷的方式檢測(cè)套接字是否可讀可寫是一種效率比較低的方式,不管是在應(yīng)用層中遍歷還是在內(nèi)核中遍歷。所以需要另外一種機(jī)制來優(yōu)化遍歷的方式,那就是回調(diào)函數(shù)。內(nèi)核中的套接字都對(duì)應(yīng)一個(gè)回調(diào)函數(shù),當(dāng)客戶端往套接字發(fā)送數(shù)據(jù)時(shí),內(nèi)核從網(wǎng)卡接收數(shù)據(jù)后就會(huì)調(diào)用回調(diào)函數(shù),在回調(diào)函數(shù)中維護(hù)事件列表,應(yīng)用層獲取此事件列表即可得到所有感興趣的事件。
內(nèi)核基于回調(diào)的事件檢測(cè)方式有兩種。第一種是用可讀列表readList和可寫列表writeList標(biāo)記讀寫事件,套接字的數(shù)量與readList和writeList兩個(gè)列表的長(zhǎng)度一樣,readList第一個(gè)元素標(biāo)為1則表示套接字1可讀,同理,writeList第二個(gè)元素標(biāo)為1則表示套接字2可寫。如圖所示,多個(gè)客戶端連接服務(wù)器端,當(dāng)客戶端發(fā)送數(shù)據(jù)過來時(shí),內(nèi)核從網(wǎng)卡復(fù)制數(shù)據(jù)成功后調(diào)用回調(diào)函數(shù)將readList第一個(gè)元素置為1,應(yīng)用層發(fā)送請(qǐng)求讀、寫事件列表,返回內(nèi)核包含了事件標(biāo)識(shí)的readList和writeList事件列表,進(jìn)而分表遍歷讀事件列表readList和寫事件列表writeList,對(duì)置為1的元素對(duì)應(yīng)的套接字進(jìn)行讀或?qū)懖僮?。這樣就避免了遍歷套接字的操作,但仍然有大量無用的數(shù)據(jù)(狀態(tài)為0的元素)從內(nèi)核復(fù)制到應(yīng)用層中。于是就有了第二種事件檢測(cè)方式。
內(nèi)核基于回調(diào)的事件檢測(cè)方式二如圖所示。服務(wù)器端有多個(gè)客戶端套接字連接。首先,應(yīng)用層告訴內(nèi)核每個(gè)套接字感興趣的事件。接著,當(dāng)客戶端發(fā)送數(shù)據(jù)過來時(shí),對(duì)應(yīng)會(huì)有一個(gè)回調(diào)函數(shù),內(nèi)核從網(wǎng)卡復(fù)制數(shù)據(jù)成功后即調(diào)回調(diào)函數(shù)將套接字1作為可讀事件event1加入到事件列表。同樣地,內(nèi)核發(fā)現(xiàn)網(wǎng)卡可寫時(shí)就將套接字2作為可寫事件event2添加到事件列表中。最后,應(yīng)用層向內(nèi)核請(qǐng)求讀、寫事件列表,內(nèi)核將包含了event1和event2的事件列表返回應(yīng)用層,應(yīng)用層通過遍歷事件列表得知套接字1有數(shù)據(jù)待讀取,于是進(jìn)行讀操作,而套接字2則可以寫入數(shù)據(jù)。
上面兩種方式由操作系統(tǒng)內(nèi)核維護(hù)客戶端的所有連接并通過回調(diào)函數(shù)不斷更新事件列表,而應(yīng)用層線程只要遍歷這些事件列表即可知道可讀取或可寫入的連接,進(jìn)而對(duì)這些連接進(jìn)行讀寫操作,極大提高了檢測(cè)效率,自然處理能力也更強(qiáng)。
對(duì)于Java來說,非阻塞I/O的實(shí)現(xiàn)完全是基于操作系統(tǒng)內(nèi)核的非阻塞I/O,它將操作系統(tǒng)的非阻塞I/O的差異屏蔽并提供統(tǒng)一的API,讓我們不必關(guān)心操作系統(tǒng)。JDK會(huì)幫我們選擇非阻塞I/O的實(shí)現(xiàn)方式,例如對(duì)于Linux系統(tǒng),在支持epoll的情況下JDK會(huì)優(yōu)先選擇用epoll實(shí)現(xiàn)Java的非阻塞I/O。這種非阻塞方式的事件檢測(cè)機(jī)制就是效率最高的“內(nèi)核基于回調(diào)的事件檢測(cè)”中的第二種方式。
在了解了非阻塞模式下的事件檢測(cè)方式后,重新回到對(duì)單線程非阻塞I/O模型的討論。雖然只有一個(gè)線程,但是它通過把非阻塞讀寫操作與上面幾種檢測(cè)機(jī)制配合就可以實(shí)現(xiàn)對(duì)多個(gè)連接的及時(shí)處理,而不會(huì)因?yàn)槟硞€(gè)連接的阻塞操作導(dǎo)致其他連接無法處理。在客戶端連接大多數(shù)都保持活躍的情況下,這個(gè)線程會(huì)一直循環(huán)處理這些連接,它很好地利用了阻塞的時(shí)間,大大提高了這個(gè)線程的執(zhí)行效率。
單線程非阻塞I/O模型的主要優(yōu)勢(shì)體現(xiàn)在對(duì)多個(gè)連接的管理,一般在同時(shí)需要處理多個(gè)連接的發(fā)場(chǎng)景中會(huì)使用非阻塞NIO模式,此模型下只通過一個(gè)線程去維護(hù)和處理連接,這樣大大提高了機(jī)器的效率。一般服務(wù)器端才會(huì)使用NIO模式,而對(duì)于客戶端,出于方便及習(xí)慣,可使用阻塞模式的套接字進(jìn)行通信。
多線程非阻塞I/O模型
單線程非阻塞I/O模型已經(jīng)大大提高了機(jī)器的效率,而在多核的機(jī)器上可以通過多線程繼續(xù)提高機(jī)器效率。最樸實(shí)、最自然的做法就是將客戶端連接按組分配給若干線程,每個(gè)線程負(fù)責(zé)處理對(duì)應(yīng)組內(nèi)的連接。如圖所示,有4個(gè)客戶端訪問服務(wù)器,服務(wù)器將套接字1和套接字2交由線程1管理,而線程2則管理套接字3和套接字4,通過事件檢測(cè)及非阻塞讀寫就可以讓每個(gè)線程都能高效處理。
最經(jīng)典的多線程非阻塞I/O模型方式是Reactor模式。首先看單線程下的Reactor,Reactor將服務(wù)器端的整個(gè)處理過程分成若干個(gè)事件,例如分為接收事件、讀事件、寫事件、執(zhí)行事件等。Reactor通過事件檢測(cè)機(jī)制將這些事件分發(fā)給不同處理器去處理。如圖所示,若干客戶端連接訪問服務(wù)器端,Reactor負(fù)責(zé)檢測(cè)各種事件并分發(fā)到處理器,這些處理器包括接收連接的accept處理器、讀數(shù)據(jù)的read處理器、寫數(shù)據(jù)的write處理器以及執(zhí)行邏輯的process處理器。在整個(gè)過程中只要有待處理的事件存在,即可以讓Reactor線程不斷往下執(zhí)行,而不會(huì)阻塞在某處,所以處理效率很高。
基于單線程Reactor模型,根據(jù)實(shí)際使用場(chǎng)景,把它改進(jìn)成多線程模式。常見的有兩種方式:一種是在耗時(shí)的process處理器中引入多線程,如使用線程池;另一種是直接使用多個(gè)Reactor實(shí)例,每個(gè)Reactor實(shí)例對(duì)應(yīng)一個(gè)線程。
Reactor模式的一種改進(jìn)方式如圖所示。其整體結(jié)構(gòu)基本上與單線程的Reactor類似,只是引入了一個(gè)線程池。由于對(duì)連接的接收、對(duì)數(shù)據(jù)的讀取和對(duì)數(shù)據(jù)的寫入等操作基本上都耗時(shí)較少,因此把它們都放到Reactor線程中處理。然而,對(duì)于邏輯處理可能比較耗時(shí)的工作,可以在process處理器中引入線程池,process處理器自己不執(zhí)行任務(wù),而是交給線程池,從而在Reactor線程中避免了耗時(shí)的操作。將耗時(shí)的操作轉(zhuǎn)移到線程池中后,盡管Reactor只有一個(gè)線程,它也能保證Reactor的高效。
Reactor模式的另一種改進(jìn)方式如圖所示。其中有多個(gè)Reactor實(shí)例,每個(gè)Reactor實(shí)例對(duì)應(yīng)一個(gè)線程。因?yàn)榻邮帐录窍鄬?duì)于服務(wù)器端而言的,所以客戶端的連接接收工作統(tǒng)一由一個(gè)accept處理器負(fù)責(zé),accept處理器會(huì)將接收的客戶端連接均勻分配給所有Reactor實(shí)例,每個(gè)Reactor實(shí)例負(fù)責(zé)處理分配到該Reactor上的客戶端連接,包括連接的讀數(shù)據(jù)、寫數(shù)據(jù)和邏輯處理。這就是多Reactor實(shí)例的原理。
多線程非阻塞I/O模式讓服務(wù)器端處理能力得到很大提高,它充分利用機(jī)器的CPU,適合用于處理高并發(fā)的場(chǎng)景,但它也讓程序更復(fù)雜,更容易出現(xiàn)問題。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9596瀏覽量
86986 -
多線程
+關(guān)注
關(guān)注
0文章
279瀏覽量
20244 -
阻塞
+關(guān)注
關(guān)注
0文章
24瀏覽量
8224 -
非阻塞
+關(guān)注
關(guān)注
0文章
13瀏覽量
2242 -
單線程
+關(guān)注
關(guān)注
0文章
18瀏覽量
1815
原文標(biāo)題:最全服務(wù)器模型詳解——從單線程阻塞到多線程非阻塞
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
鴻蒙原生應(yīng)用開發(fā)-ArkTS語言基礎(chǔ)類庫多線程I/O密集型任務(wù)開發(fā)
Linux設(shè)備驅(qū)動(dòng)中的阻塞與非阻塞I/O
阻塞與非阻塞I/O詳解
阻塞與非阻塞I/O
使用QEMU運(yùn)行RT-Thread多線程非阻塞網(wǎng)絡(luò)編程
LabVIEW中使用多線程運(yùn)行速度是否會(huì)更快
探討一下Linux系統(tǒng)下的五種I/O模型
Java多線程總結(jié)之Queue

多線程好還是單線程好?單線程和多線程的區(qū)別 優(yōu)缺點(diǎn)分析
Nodejs搭建的異步非阻塞服務(wù)器與傳統(tǒng)的阻塞多線程服務(wù)器區(qū)別

多線程服務(wù)器編程模型:如何正確使用mutex 和condition variable

Redis為何選擇單線程
鴻蒙OS開發(fā)實(shí)例:【ArkTS類庫多線程I/O密集型任務(wù)開發(fā)】

評(píng)論