兩種高效的事件處理模式
服務(wù)器程序通常需要處理三類事件:I/O 事件、信號及定時事件。有兩種高效的事件處理模式:Reactor和 Proactor,同步 I/O 模型通常用于實現(xiàn)Reactor 模式,異步 I/O 模型通常用于實現(xiàn) Proactor 模式。
無論是 Reactor,還是 Proactor,都是一種基于「事件分發(fā)」的網(wǎng)絡(luò)編程模式,區(qū)別在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式則是基于「已完成」的 I/O 事件
Reactor可以理解為:來了事件是由操作系統(tǒng)通知應(yīng)用程序,讓應(yīng)用程序來處理。又因為【內(nèi)核中的數(shù)據(jù)準備階段和數(shù)據(jù)就緒并由內(nèi)核態(tài)切換到用戶態(tài)】這兩個過程對應(yīng)用層來說是需要主動調(diào)用read來等待的,所以Reactor一般都是用同步IO實現(xiàn)。
Proactor可以理解為:來了事件是由操作系統(tǒng)處理好了之后再通知應(yīng)用程序。又因為【內(nèi)核中會由異步線程把數(shù)據(jù)準備好并且處理好了之后,直接通過信號中斷告訴應(yīng)用層】,對于應(yīng)用程序來說,得到的是已經(jīng)完成讀寫的事件,這個過程不需要等待。因此Proactor一般都是用異步IO實現(xiàn)的。
Linux基本上逐步實現(xiàn)了POSIX兼容,但并沒有參加正式的POSIX認證。由于Linux下的異步IO不完善,aio_read,aio_write系列函數(shù)是由POSIX定義的異步操作接口,不是真正操作系統(tǒng)級別支持的,而是在用戶空間模擬出來的異步,并且僅支持本地文件的aio異步操作,網(wǎng)絡(luò)編程中的socket是不支持的。所以本文沒有講述異步IO實現(xiàn)Proactor模式,取而代之的是同步IO模擬實現(xiàn)Proactor模式。
①同步IO實現(xiàn)reactor模式:主線程只負責監(jiān)聽lfd,accept成功之后把新創(chuàng)建的cfd交給子線程。子線程再通過IO多路復(fù)用去監(jiān)聽cfd的讀寫數(shù)據(jù),并且處理客戶端業(yè)務(wù)。(主線程把已被觸發(fā)但是還未完成的事件分發(fā)給子線程)
·································································································
②同步IO模擬proactor模式:是主線程accpet監(jiān)聽lfd,并且lfd讀事件觸發(fā)時,建立連接并創(chuàng)建cfd,并且通過epoll_ctl把cfd注冊到內(nèi)核的監(jiān)聽樹中,等到該socket的讀事件就緒時,主線程進行讀操作,把讀到的內(nèi)容交給子線程去進行業(yè)務(wù)處理,然后子線程處理完業(yè)務(wù)之后把該socketfd又注冊為寫時間就緒,并且把數(shù)據(jù)交回給主線程,由主線程寫回給客戶端。
(主線程模擬真實Proactor模式中的異步線程,把已完成的事件分發(fā)給子線程)
下面的并發(fā)模型中,常用的是:
1??模型四(同步IO模擬proactor模式)
2??模型五線程池版本 (同步IO實現(xiàn)reactor模式)
在客戶端數(shù)量非常多的時候適合用模型五,但是在客戶端數(shù)量不多的時候使用模型四可能會效率更好,因為模型四的線程數(shù)量更少,減少CPU切換線程的頻率。
為什么要用同步IO模擬proactor模式呢?
理論上 Proactor 比 Reactor 效率要高一些,異步 I/O 能夠充分利用 DMA 特性,讓 I/O 操作與計算重疊,但要實現(xiàn)真正的異步 I/O,操作系統(tǒng)需要做大量的工作。目前 Windows 下通過 IOCP 實現(xiàn)了真正的異步 I/O,而在 Linux 系統(tǒng)下的 AIO 并不完善,因此在 Linux 下實現(xiàn)高并發(fā)網(wǎng)絡(luò)編程時都是以 Reactor 模式為主。所以即使 Boost.Asio 號稱實現(xiàn)了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模擬出來的異步模型
服務(wù)器開發(fā)常見的并發(fā)模型
只要是做服務(wù)器開發(fā),那么常見的模型是通用的,C/C++/go等等都是通用的,因為這是一種設(shè)計思想。
其中模型四和模型五是實際開發(fā)中主流的,而模型六過于理想化目前的硬件無法實現(xiàn)。
模型一:單線程accept(無IO復(fù)用)
模型分析:
- ①主線程執(zhí)行阻塞accept,每次客戶端connect請求連接過來,主線程中的accept響應(yīng)并建立連接
- ②創(chuàng)建連接成功之后,得到新的套接字文件描述符cfd(用于與客戶端通信),然后在主線程串行處理套接字讀寫,并處理業(yè)務(wù)。
- ③在②的處理業(yè)務(wù)時,如果有新的客戶端發(fā)送請求連接,會被阻塞,服務(wù)器無響應(yīng),直到當前的cfd全部業(yè)務(wù)處理完畢,重新回到accept阻塞監(jiān)聽狀態(tài)時,才會從請求隊列中選取第一個lfd進行連接。
優(yōu)缺點:
優(yōu)點:
- socket編程流程清晰且簡單,適合學習使用,了解socket基本編程流程。
缺點:
- 該模型并非并發(fā)模型,是串行的服務(wù)器,同一時刻,監(jiān)聽并響應(yīng)最大的網(wǎng)絡(luò)請求量為1。即并發(fā)量為1。
- 僅適合學習基本socket編程,不適合任何服務(wù)器Server構(gòu)建。
模型二:單線程accept + 多線程讀寫業(yè)務(wù)(無IO復(fù)用)
模型分析:
- ①主線程執(zhí)行accept阻塞監(jiān)聽,每當有客戶端connect連接請求過來,主線程中的accept響應(yīng)并且與客戶端建立連接
- ②創(chuàng)建連接成功后得到新的cfd,然后再thread_create一個新的線程用來處理客戶端的讀寫業(yè)務(wù),并且主線程馬上回到accept阻塞監(jiān)聽繼續(xù)等待新客戶端的連接請求
- ③這個新的線程通過套接字cfd與客戶端進行通信讀寫
- ④服務(wù)器在②處理業(yè)務(wù)中,如果有新客戶端發(fā)送申請連接過來,主線程accept依然會響應(yīng)并且簡歷連接,重復(fù)②過程。
優(yōu)缺點:
優(yōu)點:
- 基于模型一作了改進,支持了并發(fā)
- 使用靈活,一個client對應(yīng)一個thread單獨處理,server處理業(yè)務(wù)的內(nèi)聚程度高(一個好的內(nèi)聚模塊應(yīng)當恰好做一件事)??蛻舳藷o論如何寫,服務(wù)端都會有一個線程做資源響應(yīng)。
缺點:
- 隨著客戶端的數(shù)量增多,需要開辟的線程也增加,客戶端與服務(wù)端線程數(shù)量是1:1正比關(guān)系。因此對于高并發(fā)場景,線程數(shù)量收到硬件的瓶頸制約。線程過多也會增加CPU的切換成本,降低CPU的利用率。
- 對于長連接,客戶端一旦沒有業(yè)務(wù)讀寫操作,只要客戶端不關(guān)閉,服務(wù)端的對應(yīng)線程就必須要保持連接(心跳包、健康監(jiān)測等機制),占用連接資源和線程的開銷
- 僅適合客戶端數(shù)量不大,并且是可控的場景來使用
- 僅適合學習基本的socket編程,不適合做并發(fā)服務(wù)器
模型三:單線程多路IO復(fù)用
模型分析:
- ①主線程main thread 創(chuàng)建 lfd之后,采用多路IO復(fù)用機制(如select和epoll)進行IO狀態(tài)阻塞監(jiān)聽。有client1客戶端 connect 請求, IO復(fù)用機制檢測到lfd觸發(fā)事件讀寫,則進行accept建立連接,并將新生成的cfd1加入到監(jiān)聽IO集合中。
- ②client1 再次進行正常讀寫業(yè)務(wù)請求,主線程的多路IO復(fù)用機制阻塞返回,主線程與client1進行讀寫通信業(yè)務(wù)。等到讀寫業(yè)務(wù)結(jié)束后,會再次返回多路IO復(fù)用的地方進行阻塞監(jiān)聽。
- ③如果client1正在進行讀寫業(yè)務(wù)時,server依然在主線程執(zhí)行流程中繼續(xù)執(zhí)行,此時如果有新的客戶端申請連接請求,server將沒有辦法及時響應(yīng)(因為是單線程,server正在讀寫),將會把這些還沒來得及響應(yīng)的請求加入阻塞隊列中。
- ④等到server處理完一個客戶端連接的讀寫操作時,繼續(xù)回到多路IO復(fù)用機制處阻塞,其他的連接如果再發(fā)送連接請求過來的話,會繼續(xù)重復(fù)②③流程。
優(yōu)缺點:
優(yōu)點:
- 單線程/單進程解決了可以同時監(jiān)聽多個客戶端讀寫狀態(tài)的模型,不需要1:1與客戶端的線程數(shù)量關(guān)系。而是1:n;
- 多路IO復(fù)用阻塞,不需要一直輪詢,所以不會浪費CPU資源,CPU利用效率較高。
缺點:
- 因為是單線程/單線程,雖然可以監(jiān)聽多個客戶端的讀寫狀態(tài),但是在同一時間內(nèi),只能處理一個客戶端的讀寫操作,實際上讀寫的業(yè)務(wù)并發(fā)為1;
- 多客戶端訪問服務(wù)器,但是業(yè)務(wù)為串行執(zhí)行,大量請求會有排隊延遲現(xiàn)象。如圖中⑤所示,當client3占據(jù)主線程流程時, client1和client2流程會卡在IO復(fù)用,等待下次監(jiān)聽觸發(fā)事件。
是否滿足實際開發(fā)?
可以!該模型編寫代碼較簡單,雖然有延遲現(xiàn)象,但是畢竟多路IO復(fù)用機制阻塞,不會占用CPU資源,如果并發(fā)請求量比較小,客戶端數(shù)量可數(shù),允許信息有一點點延遲,可以使用該模型。
比如Redis就是采用該模型設(shè)計的,因為Redis業(yè)務(wù)處理主要是在內(nèi)存中完成的,操作速度很快,性能瓶頸不在CPU上。
模型四:單線程多路IO復(fù)用 + 多線程業(yè)務(wù)工作池
模型分析:
前兩步跟模型三一致
- ①主線程main thread 創(chuàng)建 lfd之后,采用多路IO復(fù)用機制(如select和epoll)進行IO狀態(tài)阻塞監(jiān)聽。有client1客戶端 connect 請求, IO復(fù)用機制檢測到lfd觸發(fā)事件讀寫,則進行accept建立連接,并將新生成的cfd1加入到監(jiān)聽IO集合中。
- ②當cfd1有可讀消息,觸發(fā)讀事件,并且進行讀寫消息。
- ③主線程按照固定的協(xié)議讀取消息,并且交給worker pool工作線程池,工作線程池在server啟動之前就已經(jīng)開啟固定數(shù)量的線程,里面的線程只處理消息業(yè)務(wù),不進行套接字讀寫操作。
- ④工作池處理完業(yè)務(wù),觸發(fā)cfd1寫事件,將要回發(fā)客戶端的數(shù)據(jù)消息通過主線程寫回給客戶端
優(yōu)缺點:
優(yōu)點:
- 相比于模型三而言,設(shè)計了一個worker pool業(yè)務(wù)線程池,將業(yè)務(wù)處理部分從主線程抽離出來,為主線程分擔了業(yè)務(wù)處理的工作,減少了因為單線程的串行執(zhí)行業(yè)務(wù)機制,多客戶端對server的大量請求造成排隊延遲的時間。就是說主線程讀完數(shù)據(jù)之后馬上就丟給了線程池去處理,然后馬上回到多路IO復(fù)用的阻塞監(jiān)聽狀態(tài)。縮短了其他客戶端的等待連接時間。
- 由于是單線程,實際上讀寫的業(yè)務(wù)并發(fā)還是為1,但是業(yè)務(wù)流程的并發(fā)數(shù)為worker pool線程池里的線程數(shù)量,加快了業(yè)務(wù)處理并行效率。
缺點:
- 讀寫依然是主線程單獨處理,最高的讀寫并行通道依然是1,導(dǎo)致當前服務(wù)器的并發(fā)性能依然沒有提升,只是響應(yīng)任務(wù)的速度快了。每個客戶端的排隊時間短了,但因為還是只有一個通道進行讀寫操作,因此總體的完成度跟模型3是差不多的。
- 雖然多個worker線程池處理業(yè)務(wù),但是最后返回給客戶端依舊也需要排隊。因為出口還是只有read+write 這1個通道。因此業(yè)務(wù)是可以并行了,但是總體的效率是不變的。
- 模型三是客戶端向server發(fā)起請求時需要排隊,模型四是業(yè)務(wù)處理完之后回寫客戶端需要排隊。
是否滿足實際開發(fā)?
可以!模型三跟模型四的總體并發(fā)效率差不多,因為還是一個線程進行讀寫。但是對于客戶端的體驗來說,會覺得響應(yīng)速度變快,減少了在服務(wù)器的排隊時間。如果客戶端數(shù)量不多,并且各個客戶端的邏輯業(yè)務(wù)有并行需求的話適合用該模型。
模型五:單線程多路IO復(fù)用 + 多線程多路IO復(fù)用(線程池)實際中最常用
模型分析:
- ①server在啟動監(jiān)聽之前,需要創(chuàng)建固定數(shù)量N的線程,作為thread pool線程池。
- ②主線程創(chuàng)建lfd之后,采用多路IO復(fù)用機制(如select、epoll)進行IO狀態(tài)阻塞監(jiān)聽。有client1客戶端 connect請求,IO復(fù)用機制檢測到lfd觸發(fā)讀事件,則進行accept建立連接,并且將新創(chuàng)建的cfd1分發(fā)給thread pool線程池中的某個線程監(jiān)聽。
- ③thread pool中的每個thread都啟動多路IO復(fù)用機制,用來監(jiān)聽主線程建立成功并且分發(fā)下來的socket套接字(cfd)。
- ④如圖,thread1監(jiān)聽cfd1、cfd2,thread2監(jiān)聽cfd3,thread3監(jiān)聽cfd4。線程池里的每一個線程相當于它們所監(jiān)聽的客戶端所對應(yīng)的服務(wù)端。當對應(yīng)的cfd有讀寫事件時,對應(yīng)的線程池里的thread會處理相應(yīng)的讀寫業(yè)務(wù)。
優(yōu)缺點:
優(yōu)點:
- 將主線程的單流程讀寫,分散到線程池完成,這樣增加了同一時刻的讀寫并行通道,并行通道數(shù)量等于線程池的thread數(shù)量N;
- server同時監(jiān)聽cfd套接字數(shù)量幾乎成倍增大,之前的全部監(jiān)控數(shù)量取決于主線程的多路IO復(fù)用機制的最大限制(select默認1024,epoll默認與內(nèi)存有關(guān),約3~6w不等)。所以該模型的理論單點server最高的響應(yīng)并發(fā)數(shù)量為N*(3 ~ 6w)。(N為線程池thread的數(shù)量,建議與cpu核心數(shù)一致)
- 如果良好的線程池數(shù)量和CPU核心數(shù)適配,那么可以嘗試CPU核心與thread綁定,從而降低cpu的切換頻率,提高了每個thread處理業(yè)務(wù)的效率。
缺點:
- 雖然監(jiān)聽的并發(fā)數(shù)量提升,但是最高讀寫并行通道依然為N,而且多個身處被同一個thread所監(jiān)聽的客戶端也會出現(xiàn)延遲讀寫現(xiàn)象。實際上線程池里每個thread對應(yīng)客戶端的部分,相當于模型三。
是否滿足實際開發(fā)?
可以!當前主流的線程池框架就是模型五,其中有Netty 和 Memcache 。
模型六:(多進程版)單線程多路IO復(fù)用 + 多進程多路IO復(fù)用(進程池)
模型分析:
與線程池版沒有太大的差異。需要在服務(wù)器啟動之前先創(chuàng)建一些守護進程在后臺運行。
存在的不同之處:
- ①進程間資源不共享,而線程是共享資源的。進程和線程的內(nèi)存布局不同導(dǎo)致主進程不再進行accept操作,而是將accept過程分散到每一個子進程中
- ②進程的資源獨立,所以主進程如果accept成功cfd,其他的進程是沒有辦法共享資源的,因此需要各子進程自行accpet創(chuàng)建連接
- ③主進程只是監(jiān)聽listenFd狀態(tài),一旦觸發(fā)讀事件或者有新連接請求,通過IPC進程間通信(signal、mmap、fifo等方式)讓所有的子進程們進行競爭,搶到lfd讀事件資源的子進程會進行accpet操作,監(jiān)聽他們自己所創(chuàng)建出來的套接字cfd。(自己創(chuàng)建的cfd,由自己監(jiān)聽cfd的讀寫事件)
優(yōu)缺點:
與線程池版本沒有太大差異
優(yōu)點:
- 由于進程間的資源獨立,盡管是父子進程,也是讀時共享,寫時復(fù)制。因此多進程模型安全穩(wěn)定性較強,各自進程互不干擾。Nginx就是使用進程池的框架實現(xiàn)的。不過方案與標準的多 Reactor 多進程有些差異。具體差異表現(xiàn)在主進程中僅僅用來初始化 socket,并沒有創(chuàng)建 mainReactor 來 accept 連接,而是由子進程的 Reactor 來 accept 連接,通過鎖來控制一次只有一個子進程進行 accept(防止出現(xiàn)驚群現(xiàn)象),子進程 accept 新連接后就放到自己的 Reactor 進行處理,不會再分配給其他子進程。
缺點:
- 多進程內(nèi)存資源空間占用得稍微大一些
模型七:單線程多路I/O復(fù)用+多線程多路I/O復(fù)用+多線程
模型分析:
- ①server在啟動監(jiān)聽之前,開辟固定數(shù)量N個線程,創(chuàng)建thread pool線程池。
- ②主線程創(chuàng)建lfd之后,采用多路IO復(fù)用機制進行IO狀態(tài)的阻塞監(jiān)聽。當有client1客戶端connect請求,多路IO復(fù)用機制檢測到lfd觸發(fā)讀事件,則會進行accept建立連接,并把accept后新創(chuàng)建的cfd1分發(fā)給thread pool中的某個線程進行監(jiān)聽。
- ③線程池中的每個thread都啟動多路IO復(fù)用機制,用來監(jiān)聽主線程分發(fā)下來的socket套接字cfd。一旦某個被監(jiān)聽的cfd被觸發(fā)了讀寫事件,該線程池里的thread會立即開辟他的一個子線程與cfd進行讀寫業(yè)務(wù)操作。
- ④當某個讀寫線程完成當前讀寫業(yè)務(wù)時,如果當前套接字沒有被關(guān)閉,那么該線程會將當前的cfd套接字重新加回線程池的監(jiān)聽線程中,同時自身銷毀。
優(yōu)缺點:
優(yōu)點:
- 在模型五的基礎(chǔ)上,除了能夠保證同時響應(yīng)的最高并發(fā)數(shù),又能解決了讀寫并行通道被局限的問題。
- 同一時刻的讀寫并行通道達到最大極限,一個客戶端可以對應(yīng)一個單獨線程處理讀寫業(yè)務(wù)。讀寫并行通道與客戶端的數(shù)量是1 :1關(guān)系。
缺點:
- 該模型過于理想化,因為要求cpu的核心數(shù)足夠大
- 如果硬件cpu數(shù)量可數(shù)(目前的硬件情況就是cpu可數(shù)),那么該模型將造成大量的cpu切換成本。為了保證讀寫并行通道與客戶端可以一對一服務(wù),那么server需要開辟的線程數(shù)量就要與客戶端一致,那么線程池中多路IO復(fù)用的監(jiān)聽線程池綁定CPU數(shù)量將會變得毫無意義。(因為使用多路IO復(fù)用機制,就是為了達到1個線程可以監(jiān)聽多個client。如果現(xiàn)在的線程數(shù)量已經(jīng)跟客戶端數(shù)量一致了,那多路IO復(fù)用就沒意義了)
- 如果每個臨時的讀寫線程都能夠綁定一個單獨的CPU,那么此模型將會是最優(yōu)模型。但是目前的CPU數(shù)量無法與客戶端的數(shù)量達到一個量級,還差得遠。
八 、總結(jié)
- 上面整理了7種server的服務(wù)器處理結(jié)構(gòu)模型,對于應(yīng)付高并發(fā)和高CPU利用率的模型,目前采用最多的是模型五,其中Nginx就是類似模型五進程版的改版。
- 并發(fā)模型并且設(shè)計得越復(fù)雜越好,也不是線程開辟越多越好。真實設(shè)計開發(fā)中需要考慮硬件的利用和CPU切換成本的開銷。模型六的設(shè)計極為復(fù)雜,線程較多,但以當今的硬件能力無法實現(xiàn),反倒導(dǎo)致該模型性能級差。所以對于不同的業(yè)務(wù)場景要選擇適合的模型構(gòu)建,并不是說固定要使用哪一個,要根據(jù)實際靈活變動。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9184瀏覽量
85489 -
Server
+關(guān)注
關(guān)注
0文章
90瀏覽量
24047 -
模型
+關(guān)注
關(guān)注
1文章
3248瀏覽量
48864 -
應(yīng)用程序
+關(guān)注
關(guān)注
37文章
3271瀏覽量
57727
發(fā)布評論請先 登錄
相關(guān)推薦
評論