天天看點

socket server伺服器開發常見的并發模型

作者:linux技術棧

兩種高效的事件處理模式

伺服器程式通常需要處理三類事件:I/O 事件、信号及定時事件。有兩種高效的事件處理模式:Reactor和 Proactor,同步 I/O 模型通常用于實作Reactor 模式,異步 I/O 模型通常用于實作 Proactor 模式。

無論是 Reactor,還是 Proactor,都是一種基于「事件分發」的網絡程式設計模式,差別在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式則是基于「已完成」的 I/O 事件 Reactor可以了解為:來了事件是由作業系統通知應用程式,讓應用程式來處理。又因為【核心中的資料準備階段和資料就緒并由核心态切換到使用者态】這兩個過程對應用層來說是需要主動調用read來等待的,是以Reactor一般都是用同步IO實作。 Proactor可以了解為:來了事件是由作業系統處理好了之後再通知應用程式。又因為【核心中會由異步線程把資料準備好并且處理好了之後,直接通過信号中斷告訴應用層】,對于應用程式來說,得到的是已經完成讀寫的事件,這個過程不需要等待。是以Proactor一般都是用異步IO實作的。

Linux基本上逐漸實作了POSIX相容,但并沒有參加正式的POSIX認證。由于Linux下的異步IO不完善,aio_read,aio_write系列函數是由POSIX定義的異步操作接口,不是真正作業系統級别支援的,而是在使用者空間模拟出來的異步,并且僅支援本地檔案的aio異步操作,網絡程式設計中的socket是不支援的。是以本文沒有講述異步IO實作Proactor模式,取而代之的是同步IO模拟實作Proactor模式。

①同步IO實作reactor模式:主線程隻負責監聽lfd,accept成功之後把新建立的cfd交給子線程。子線程再通過IO多路複用去監聽cfd的讀寫資料,并且處理用戶端業務。(主線程把已被觸發但是還未完成的事件分發給子線程) ································································································· ②同步IO模拟proactor模式:是主線程accpet監聽lfd,并且lfd讀事件觸發時,建立連接配接并建立cfd,并且通過epoll_ctl把cfd注冊到核心的監聽樹中,等到該socket的讀事件就緒時,主線程進行讀操作,把讀到的内容交給子線程去進行業務處理,然後子線程處理完業務之後把該socketfd又注冊為寫時間就緒,并且把資料交回給主線程,由主線程寫回給用戶端。 (主線程模拟真實Proactor模式中的異步線程,把已完成的事件分發給子線程)

下面的并發模型中,常用的是:

1️⃣模型四(同步IO模拟proactor模式)

2️⃣模型五線程池版本 (同步IO實作reactor模式)

在用戶端數量非常多的時候适合用模型五,但是在用戶端數量不多的時候使用模型四可能會效率更好,因為模型四的線程數量更少,減少CPU切換線程的頻率。

為什麼要用同步IO模拟proactor模式呢? 理論上 Proactor 比 Reactor 效率要高一些,異步 I/O 能夠充分利用 DMA 特性,讓 I/O 操作與計算重疊,但要實作真正的異步 I/O,作業系統需要做大量的工作。目前 Windows 下通過 IOCP 實作了真正的異步 I/O,而在 Linux 系統下的 AIO 并不完善,是以在 Linux 下實作高并發網絡程式設計時都是以 Reactor 模式為主。是以即使 Boost.Asio 号稱實作了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模拟出來的異步模型

伺服器開發常見的并發模型

隻要是做伺服器開發,那麼常見的模型是通用的,C/C++/go等等都是通用的,因為這是一種設計思想。

其中模型四和模型五是實際開發中主流的,而模型六過于理想化目前的硬體無法實作。

模型一:單線程accept(無IO複用)

socket server伺服器開發常見的并發模型

模型分析:

  • ①主線程執行阻塞accept,每次用戶端connect請求連接配接過來,主線程中的accept響應并建立連接配接
  • ②建立連接配接成功之後,得到新的套接字檔案描述符cfd(用于與用戶端通信),然後在主線程串行處理套接字讀寫,并處理業務。
  • ③在②的處理業務時,如果有新的用戶端發送請求連接配接,會被阻塞,伺服器無響應,直到目前的cfd全部業務處理完畢,重新回到accept阻塞監聽狀态時,才會從請求隊列中選取第一個lfd進行連接配接。

優缺點:

優點:

  • socket程式設計流程清晰且簡單,适合學習使用,了解socket基本程式設計流程。

缺點:

  • 該模型并非并發模型,是串行的伺服器,同一時刻,監聽并響應最大的網絡請求量為1。 即并發量為1。
  • 僅适合學習基本socket程式設計,不适合任何伺服器Server建構。

相關視訊推薦

準備好4台虛拟機,實作伺服器的百萬級并發

6種epoll的設計方法(單線程epoll、多線程epoll、多程序epoll)及應用場景

手把手實作線程池(120行),實作異步操作,解決項目性能問題

需要C/C++ Linux伺服器架構師學習資料加qun812855908擷取(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

socket server伺服器開發常見的并發模型

模型二:單線程accept + 多線程讀寫業務(無IO複用)

socket server伺服器開發常見的并發模型

​模型分析:

  • ①主線程執行accept阻塞監聽,每當有用戶端connect連接配接請求過來,主線程中的accept響應并且與用戶端建立連接配接
  • ②建立連接配接成功後得到新的cfd,然後再thread_create一個新的線程用來處理用戶端的讀寫業務,并且主線程馬上回到accept阻塞監聽繼續等待新用戶端的連接配接請求
  • ③這個新的線程通過套接字cfd與用戶端進行通信讀寫
  • ④伺服器在②處理業務中,如果有新用戶端發送申請連接配接過來,主線程accept依然會響應并且履歷連接配接,重複②過程。

優缺點:

優點:

  • 基于模型一作了改進,支援了并發
  • 使用靈活,一個client對應一個thread單獨處理,server處理業務的内聚程度高(一個好的内聚子產品應當恰好做一件事)。用戶端無論如何寫,服務端都會有一個線程做資源響應。

缺點:

  • 随着用戶端的數量增多,需要開辟的線程也增加,用戶端與服務端線程數量是1:1正比關系。是以對于高并發場景,線程數量收到硬體的瓶頸制約。線程過多也會增加CPU的切換成本,降低CPU的使用率。
  • 對于長連接配接,用戶端一旦沒有業務讀寫操作,隻要用戶端不關閉,服務端的對應線程就必須要保持連接配接(心跳包、健康監測等機制),占用連接配接資源和線程的開銷
  • 僅适合用戶端數量不大,并且是可控的場景來使用
  • 僅适合學習基本的socket程式設計,不适合做并發伺服器

模型三:單線程多路IO複用

socket server伺服器開發常見的并發模型

​模型分析:

  • ①主線程main thread 建立 lfd之後,采用多路IO複用機制(如select和epoll)進行IO狀态阻塞監聽。有client1用戶端 connect 請求, IO複用機制檢測到lfd觸發事件讀寫,則進行accept建立連接配接,并将新生成的cfd1加入到監聽IO集合中。
  • ②client1 再次進行正常讀寫業務請求,主線程的多路IO複用機制阻塞傳回,主線程與client1進行讀寫通信業務。等到讀寫業務結束後,會再次傳回多路IO複用的地方進行阻塞監聽。
  • ③如果client1正在進行讀寫業務時,server依然在主線程執行流程中繼續執行,此時如果有新的用戶端申請連接配接請求,server将沒有辦法及時響應(因為是單線程,server正在讀寫),将會把這些還沒來得及響應的請求加入阻塞隊列中。
  • ④等到server處理完一個用戶端連接配接的讀寫操作時,繼續回到多路IO複用機制處阻塞,其他的連接配接如果再發送連接配接請求過來的話,會繼續重複②③流程。

優缺點:

優點:

  • 單線程/單程序解決了可以同時監聽多個用戶端讀寫狀态的模型,不需要1:1與用戶端的線程數量關系。而是1:n;
  • 多路IO複用阻塞,不需要一直輪詢,是以不會浪費CPU資源,CPU利用效率較高。

缺點:

  • 因為是單線程/單線程,雖然可以監聽多個用戶端的讀寫狀态,但是在同一時間内,隻能處理一個用戶端的讀寫操作,實際上讀寫的業務并發為1;
  • 多用戶端通路伺服器,但是業務為串行執行,大量請求會有排隊延遲現象。如圖中⑤所示,當client3占據主線程流程時, client1和client2流程會卡在IO複用,等待下次監聽觸發事件。

是否滿足實際開發?

可以!該模型編寫代碼較簡單,雖然有延遲現象,但是畢竟多路IO複用機制阻塞,不會占用CPU資源,如果并發請求量比較小,用戶端數量可數,允許資訊有一點點延遲,可以使用該模型。

比如Redis就是采用該模型設計的,因為Redis業務處理主要是在記憶體中完成的,操作速度很快,性能瓶頸不在CPU上。

模型四:單線程多路IO複用 + 多線程業務工作池

socket server伺服器開發常見的并發模型

​模型分析:

前兩步跟模型三一緻

  • ①主線程main thread 建立 lfd之後,采用多路IO複用機制(如select和epoll)進行IO狀态阻塞監聽。有client1用戶端 connect 請求, IO複用機制檢測到lfd觸發事件讀寫,則進行accept建立連接配接,并将新生成的cfd1加入到監聽IO集合中。
  • ②當cfd1有可讀消息,觸發讀事件,并且進行讀寫消息。
  • ③主線程按照固定的協定讀取消息,并且交給worker pool工作線程池,工作線程池在server啟動之前就已經開啟固定數量的線程,裡面的線程隻處理消息業務,不進行套接字讀寫操作。
  • ④工作池處理完業務,觸發cfd1寫事件,将要回發用戶端的資料消息通過主線程寫回給用戶端

優缺點:

優點:

  • 相比于模型三而言,設計了一個worker pool業務線程池,将業務處理部分從主線程抽離出來,為主線程分擔了業務處理的工作,減少了因為單線程的串行執行業務機制,多用戶端對server的大量請求造成排隊延遲的時間。就是說主線程讀完資料之後馬上就丢給了線程池去處理,然後馬上回到多路IO複用的阻塞監聽狀态。縮短了其他用戶端的等待連接配接時間。
  • 由于是單線程,實際上讀寫的業務并發還是為1,但是業務流程的并發數為worker pool線程池裡的線程數量,加快了業務處理并行效率。

缺點:

  • 讀寫依然是主線程單獨處理,最高的讀寫并行通道依然是1,導緻目前伺服器的并發性能依然沒有提升,隻是響應任務的速度快了。每個用戶端的排隊時間短了,但因為還是隻有一個通道進行讀寫操作,是以總體的完成度跟模型3是差不多的。
  • 雖然多個worker線程池處理業務,但是最後傳回給用戶端依舊也需要排隊。因為出口還是隻有read+write 這1個通道。是以業務是可以并行了,但是總體的效率是不變的。
  • 模型三是用戶端向server發起請求時需要排隊,模型四是業務處理完之後回寫用戶端需要排隊。

是否滿足實際開發?

可以! 模型三跟模型四的總體并發效率差不多,因為還是一個線程進行讀寫。但是對于用戶端的體驗來說,會覺得響應速度變快,減少了在伺服器的排隊時間。如果用戶端數量不多,并且各個用戶端的邏輯業務有并行需求的話适合用該模型。

模型五:單線程多路IO複用 + 多線程多路IO複用(線程池)實際中最常用

socket server伺服器開發常見的并發模型

​模型分析:

  • ①server在啟動監聽之前,需要建立固定數量N的線程,作為thread pool線程池。
  • ②主線程建立lfd之後,采用多路IO複用機制(如select、epoll)進行IO狀态阻塞監聽。有client1用戶端 connect請求,IO複用機制檢測到lfd觸發讀事件,則進行accept建立連接配接,并且将新建立的cfd1分發給thread pool線程池中的某個線程監聽。
  • ③thread pool中的每個thread都啟動多路IO複用機制,用來監聽主線程建立成功并且分發下來的socket套接字(cfd)。
  • ④如圖,thread1監聽cfd1、cfd2,thread2監聽cfd3,thread3監聽cfd4。線程池裡的每一個線程相當于它們所監聽的用戶端所對應的服務端。當對應的cfd有讀寫事件時,對應的線程池裡的thread會處理相應的讀寫業務。

優缺點:

優點:

  • 将主線程的單流程讀寫,分散到線程池完成,這樣增加了同一時刻的讀寫并行通道,并行通道數量等于線程池的thread數量N;
  • server同時監聽cfd套接字數量幾乎成倍增大,之前的全部監控數量取決于主線程的多路IO複用機制的最大限制(select預設1024,epoll預設與記憶體有關,約3~6w不等)。是以該模型的理論單點server最高的響應并發數量為N*(3 ~ 6w)。(N為線程池thread的數量,建議與cpu核心數一緻)
  • 如果良好的線程池數量和CPU核心數适配,那麼可以嘗試CPU核心與thread綁定,進而降低cpu的切換頻率,提高了每個thread處理業務的效率。

缺點:

  • 雖然監聽的并發數量提升,但是最高讀寫并行通道依然為N,而且多個身處被同一個thread所監聽的用戶端也會出現延遲讀寫現象。實際上線程池裡每個thread對應用戶端的部分,相當于模型三。

是否滿足實際開發?

可以! 目前主流的線程池架構就是模型五,其中有Netty 和 Memcache 。

模型六:(多程序版)單線程多路IO複用 + 多程序多路IO複用(程序池)

socket server伺服器開發常見的并發模型

模型分析:

與線程池版沒有太大的差異。需要在伺服器啟動之前先建立一些守護程序在背景運作。

存在的不同之處:

  • ①程序間資源不共享,而線程是共享資源的。程序和線程的記憶體布局不同導緻主程序不再進行accept操作,而是将accept過程分散到每一個子程序中
  • ②程序的資源獨立,是以主程序如果accept成功cfd,其他的程序是沒有辦法共享資源的,是以需要各子程序自行accpet建立連接配接
  • ③主程序隻是監聽listenFd狀态,一旦觸發讀事件或者有新連接配接請求,通過IPC程序間通信(signal、mmap、fifo等方式)讓所有的子程序們進行競争,搶到lfd讀事件資源的子程序會進行accpet操作,監聽他們自己所建立出來的套接字cfd。(自己建立的cfd,由自己監聽cfd的讀寫事件)

優缺點:

與線程池版本沒有太大差異

優點:

  • 由于程序間的資源獨立,盡管是父子程序,也是讀時共享,寫時複制。是以多程序模型安全穩定性較強,各自程序互不幹擾。Nginx就是使用程序池的架構實作的。不過方案與标準的多 Reactor 多程序有些差異。具體差異表現在主程序中僅僅用來初始化 socket,并沒有建立 mainReactor 來 accept 連接配接,而是由子程序的 Reactor 來 accept 連接配接,通過鎖來控制一次隻有一個子程序進行 accept(防止出現驚群現象),子程序 accept 新連接配接後就放到自己的 Reactor 進行處理,不會再配置設定給其他子程序。

缺點:

  • 多程序記憶體資源空間占用得稍微大一些

模型七:單線程多路I/O複用+多線程多路I/O複用+多線程

socket server伺服器開發常見的并發模型

​模型分析:

  • ①server在啟動監聽之前,開辟固定數量N個線程,建立thread pool線程池。
  • ②主線程建立lfd之後,采用多路IO複用機制進行IO狀态的阻塞監聽。當有client1用戶端connect請求,多路IO複用機制檢測到lfd觸發讀事件,則會進行accept建立連接配接,并把accept後新建立的cfd1分發給thread pool中的某個線程進行監聽。
  • ③線程池中的每個thread都啟動多路IO複用機制,用來監聽主線程分發下來的socket套接字cfd。一旦某個被監聽的cfd被觸發了讀寫事件,該線程池裡的thread會立即開辟他的一個子線程與cfd進行讀寫業務操作。
  • ④當某個讀寫線程完成目前讀寫業務時,如果目前套接字沒有被關閉,那麼該線程會将目前的cfd套接字重新加回線程池的監聽線程中,同時自身銷毀。

優缺點:

優點:

  • 在模型五的基礎上,除了能夠保證同時響應的最高并發數,又能解決了讀寫并行通道被局限的問題。
  • 同一時刻的讀寫并行通道達到最大極限,一個用戶端可以對應一個單獨線程處理讀寫業務。讀寫并行通道與用戶端的數量是1 :1關系。

缺點:

  • 該模型過于理想化,因為要求cpu的核心數足夠大
  • 如果硬體cpu數量可數(目前的硬體情況就是cpu可數),那麼該模型将造成大量的cpu切換成本。為了保證讀寫并行通道與用戶端可以一對一服務,那麼server需要開辟的線程數量就要與用戶端一緻,那麼線程池中多路IO複用的監聽線程池綁定CPU數量将會變得毫無意義。(因為使用多路IO複用機制,就是為了達到1個線程可以監聽多個client。如果現在的線程數量已經跟用戶端數量一緻了,那多路IO複用就沒意義了)
  • 如果每個臨時的讀寫線程都能夠綁定一個單獨的CPU,那麼此模型将會是最優模型。但是目前的CPU數量無法與用戶端的數量達到一個量級,還差得遠。

八 、總結

  • 上面整理了7種server的伺服器處理結構模型,對于應付高并發和高CPU使用率的模型,目前采用最多的是模型五,其中Nginx就是類似模型五程序版的改版。
  • 并發模型并且設計得越複雜越好,也不是線程開辟越多越好。真實設計開發中需要考慮硬體的利用和CPU切換成本的開銷。模型六的設計極為複雜,線程較多,但以當今的硬體能力無法實作,反倒導緻該模型性能級差。是以對于不同的業務場景要選擇适合的模型建構,并不是說固定要使用哪一個,要根據實際靈活變動。

繼續閱讀