1. 程式人生 > >【網路程式設計】服務端的I/O模型,事件處理模式,併發模式

【網路程式設計】服務端的I/O模型,事件處理模式,併發模式

前言之前的前言

本文作於6月中旬,當時對於很多概念不是很理解,所以寫到一半實在進行不下去,通過最近的學習終於理解了一些,趕緊總結記下。

前言

本篇主要總結伺服器端開發中的一些基本的框架。
如果你在東區二樓點過黃燜雞,相信你一定能更好的理解。

正文

I/O模型

主要可以分為同步I/O非同步I/O 兩大類。

同步I/O

我們可以理解為,在I/O事件發生後(出現了I/O請求),由應用程式負責處理I/O,或者說,核心嚮應用程式通知I/O就緒事件
理論上來說,阻塞I/O(如read一直等待),I/O複用(如select,epoll),訊號I/O都是同步I/O。

非同步I/O

這倆當然是相反的咯,在I/O事件發生後(出現了I/O請求),由核心負責處理I/O,核心嚮應用程式通知I/O完成事件

,比如Linux下的aio,(還有C++的asio??)

理解

之前對於同步和非同步總是很混亂,看過之後才明白,同步I/O是由應用程式來處理,而非同步只是應用程式將要做的I/O處理提前告訴了核心,一旦需要處理,核心直接按照之前的要求完成,然後將結果告訴應用程式。

舉個例子:

康康到食堂點了一份黃燜雞,

對於同步I/O,雞做好了(I/O事件發生),小姐姐喊號(核心通知應用程式I/O就緒),康康端著雞回去了(應用程式自己處理I/O)。

而對於非同步I/O,康康在點雞時就告訴了小姐姐,請送到FZ118(將請求告訴核心),雞做好了(I/O事件發生),小姐姐直接端到FZ118(核心按照要求直接處理I/O),然後告訴康康,雞送到了(核心通知應用程式I/O完成)。

同步/非同步和阻塞/非阻塞的關係?

當初這兩個也搞不明白,現在大致清楚了。

非同步I/O自然是非阻塞,因為應用程式只需通知核心,這一事件不需要什麼前提,程式沒有阻塞。

同步I/O中的阻塞I/O,如read,沒有資料則程式一直阻塞,自然是阻塞的。
同樣,程式也會阻塞在I/O複用(如select時間內一直沒有事件發生),但是對於發生的I/O事件,並不是阻塞的,一旦出現了I/O,直接處理
訊號I/O同理,出現事件直接處理。並且訊號註冊的過程也只是和通知一樣,程式也不阻塞。

所以這倆貨沒啥直接對應關係。。。

事件處理模式

伺服器端主要需要處理三類事件,I/O事件,定時事件和訊號,首先從整體上分成兩種事件處理模式。

Reactor — 同步I/O

pic

主執行緒(I/O處理單元)負責監聽socket上的I/O事件。一旦出現(如可讀,有新的連線),則主執行緒將socket交給 工作執行緒去處理,然後自己繼續監聽,工作執行緒把各種活幹完了,所以它並不區分讀寫執行緒

這裡是在I/O出現之後,核心通知應用程式,工作執行緒最終處理I/O。自然是同步模型。

Proactor — 非同步I/O

pic

主執行緒通過epoll監聽socket,一旦有連線socket則在核心註冊讀完成事件(告訴核心讀到哪裡,讀完之後發個怎樣的通知,這裡以訊號為例)。核心讀好之後訊號發給程式,通過訊號通知工作執行緒進行業務邏輯處理,工作執行緒處理完業務邏輯後向核心註冊寫完成事件(寫到哪裡,寫完之後發個怎樣的通知)核心傳送訊號,程式再決定下一步操作。

併發模式

併發模式是指I/O處理單元和邏輯單元協調完成任務的方法。

對於計算密集型的程式,併發程式設計沒有什麼優勢(反而由於程序間的切換降低了效率),但是對於I/O密集型的程式,通過程序的排程減少了CPU等待的事件提升了效率。

在下文中,我們提到的同步非同步 是指程式碼的執行順序,同步是順序執行,而非同步是程式執行由事件驅動

半同步-半非同步模式(half-sync/half-async)

顧名思義,既有同步執行緒,又有非同步執行緒。
同步執行緒負責處理客戶邏輯,而非同步執行緒負責處理I/O事件。
最簡單的一個例子,和生產者–消費者模型類似,非同步執行緒監聽socket,一旦有請求出現,將其封裝成請求物件,放到訊息佇列中,然後佇列將請求分發給同步執行緒,根據需求處理。

一種變體—半同步-半反應堆模式(half-sync/ half-reactive)

pic

主執行緒為非同步執行緒,負責監聽socket,若有連線請求,則將其在epoll中註冊讀寫事件,如果已經連線的socket上有讀寫事件發生,則把其插入到請求佇列中。
而其餘的同步執行緒,則通過競爭的方式從佇列中獲取socket,再進行讀寫和業務邏輯的處理。
這裡是一個典型的reactor模式哦,複習一下上面的reactor模式(主執行緒只負責監聽,活都給工作執行緒了)

我覺得這就是一個半同步半非同步執行緒池,非同步指接收到socket、放入佇列,而讀寫socket,處理業務邏輯則是同步的。

缺點主要有兩點:

一、這其實就是一個典型的生產者—消費者的例子,那麼對於這個佇列的操作我們需要加鎖—一加鎖自然效率下降(耗費CPU)。

二、每個工作(同步)執行緒一次只能處理一個客戶請求,處理完才能換成下一個,那麼對於突然的大量請求。。只能靠增加執行緒來解決,這裡的問題就是大量執行緒的上下文切換又會是一步很大的開銷。
在之後的部落格中我會分享一個C++實現的執行緒池。

一種高效一點的半同步—半非同步模式

pic2

主要是針對上面的兩個缺點進行的改進。
首先訊息佇列被取消了,主執行緒在監聽socket上得到連線socket後直接傳遞給工作執行緒,而執行緒則將其註冊到自己的epoll核心事件表中,然後epoll_wait等待處理上面發生的讀寫事件。
這裡訊息佇列取消,我們不再需要加鎖,提高效率。而且每個工作執行緒可以處理多個客戶請求。
(當然,由於每個工作執行緒現在也是epoll事件驅動了。。感覺不是同步執行緒了)

領導者-追隨者模式(Leader-Follower)

這裡的Leader就是負責監聽socket的執行緒,不同的是當監聽到事件發生,Leader可以自己直接去進行處理(或者指派給Follower)
在這種模式中,執行緒一共有三種狀態,Leader、Follower、Processing。
一共有三種元件:控制代碼集、執行緒集、事件處理器。
控制代碼集就是監聽很多控制代碼(fd/socket)發生事件時通知給Leader。
每時刻只能有一個Leader等待I/O事件通知,可以有多個Follower等待工作。
然後Leader可以選擇自己通過事件處理器處理事件,也可以指派Follwer去處理。
事件處理器是各種繫結在控制代碼上的回撥函式,所以執行緒處理事件是直接執行回撥函式。
處理的執行緒為Processing狀態。
執行緒集就是排程所有執行緒,負責執行緒之間的同步,選舉Leader執行緒。
如果Leader自己去處理I/O,還要通過執行緒集去選舉下一個Leader。
當Processing處理完之後,如果沒有Leader則成為Leader否則是Follower。
與上一種高效半同步–半非同步相比,Leader直接處理請求省去了執行緒之間的訊息傳遞,但是缺點是一個執行緒依然只能處理一個客戶請求。。。

才疏學淺,不足之處,歡迎指正