目錄
在面對異步IO頻繁的業務需求的時,可以使用回調的機制。在利用回調的過程中,如果利用狀態機則會發生回調金字塔(callback hell),主要表現爲:1.代碼複用率極低。2.邏輯複雜。此時利用協同程序可以很好的解決這個問題。
CPU (CentralProcessingUnit)
計算機的核心是CPU,所有的計算任務都是由它完成。CPU概念包括:
物理CPU
物理CPU就是插在主機上的真實的CPU硬件,在Linux下可以數不同的physical id 來確認主機的物理CPU個數。
核心數
物理CPU下一層概念就是核心數,我們常常會聽說多核處理器,其中的核指的就是核心數。在Linux下可以通過cores來確認主機的物理CPU的核心數。
邏輯CPU
邏輯CPU跟超線程技術有聯繫,假如物理CPU不支持超線程的,那麼邏輯CPU的數量等於核心數的數量;如果物理CPU支持超線程,那麼邏輯CPU的數目是核心數數目的兩倍。在Linux下可以通過 processors 的數目來確認邏輯CPU的數量。
層級如下圖所示:
進程與線程
**進程是CPU資源分配的最小單位,線程是CPU調度的最小單位。**進程包含線程,常見的模型有:
1.單進程單線程模型
進程與線程的區別在於:進程掌管着資源,線程是進程的一部分,CPU執行調度的是線程。
2.單進程多線程模型
多個線程共享全局資源,但不包括線程自己的棧空間。
3.多進程單線程模型
單個應用可以通過fork拷貝出多個進程,進程的資源會進行復制。注意:fd同樣會複製,不建議多個進程共用同一個fd,會出現數據亂序的情況。
4.多進程單線程模型
涉及多線程時,鎖是個必須掌握的知識點,否則同時對共享資源進行處理可能導致覆蓋寫問題。
網絡編程中5種I/O模型
在Unix(linux)平臺下有5中I/O模型:
同步I/O模型
堵塞I/O模型(blocking I/O)
非堵塞I/O模型(un-blocking I/O)
I/O多路複用模型(select, poll, epoll)(較常見)
信號驅動I/O模型(SIGIO)
異步I/O模型
同步與異步的區別
同步:指關於這個I/O中的一系列動作都需要自己來完成,無論你是原地等待事件的發生(阻塞)還是當某個事件已經準備好的時候你去完成後面的的動作(非阻塞)都屬於同步。
異步:它是指是調用另一個執行者去完成,當執行者發現要處理的事件後調用你,你再完成這件事情,執行的過程和你的動作是不牽扯的。
因此,前四種是同步I/O模型,只有第五種是異步的。
I/O操作一般分爲兩個階段:
- 等待數據達到內核緩存區
- 將數據從內核拷貝到用戶進程
阻塞型I/O
通常把阻塞的文件描述符(file descriptor,fd)稱之爲阻塞I/O。默認條件下,創建的socket fd是阻塞的,針對阻塞I/O調用系統接口,可能因爲等待的事件沒有到達而被系統掛起,直到等待的事件觸發調用接口才返回,例如,tcp socket的recvfrom調用會阻塞至連接有數據返回,如上圖所示。另外socket 的系統API ,如,accept、send、connect等都可能被阻塞。
非阻塞型I/O
一種輪詢的機制
把非阻塞的文件描述符稱爲非阻塞I/O。可以通過設置SOCK_NONBLOCK標記創建非阻塞的socket fd,或者使用fcntl將fd設置爲非阻塞。
對非阻塞fd調用系統接口時,不需要等待事件發生而立即返回,事件沒有發生,接口返回-1,此時需要通過errno的值來區分是否出錯,有過網絡編程的經驗的應該都瞭解這點。不同的接口,立即返回時的errno值不盡相同,如,recv、send、accept errno通常被設置爲EAGIN 或者EWOULDBLOCK,connect 則爲EINPRO- GRESS 。
I/O多路複用
最常用的I/O事件通知機制就是I/O複用(I/O multiplexing)。Linux 環境中使用select/poll/epoll 實現I/O複用,I/O複用接口本身是阻塞的,在應用程序中通過I/O複用接口向內核註冊fd所關注的事件,當關注事件觸發時,通過I/O複用接口的返回值通知到應用程序,如圖3所示,以recv爲例。I/O複用接口可以同時監聽多個I/O事件以提高事件處理效率。
好處:其實這就是一個回調實現的機制。在這個過程中,只需要兩個線程就可以完成多個連接請求。一個爲業務線程,一個爲epoll模型監聽線程。這樣的話,就可以利用一個業務線程進行大量的訪問請求處理。而不必像PHP等實現機制,每一個請求都分配一個線程,之後阻塞等待。
回調機制
在這種典型的回調機制的實現過程中,通過epoll模型返回的結果即狀態機的條件,以及結合上下文即狀態機的現態,可以觸發動作。即:條件+現態->動作(狀態機是一種switch-case的結構,邏輯是非順序的,類似於一種表格的結構—詳見深入淺出理解有限狀態機),因爲狀態機的邏輯非順序化,所以將其邏輯順序化的過程,會出現callback hell的問題。如下圖所示:
解決這樣的問題協程是一種有效的手段,即:協程可以處理大量的異步I/O操作的需求業務。
信號驅動I/O
除了I/O複用方式通知I/O事件,還可以通過SIGIO信號來通知I/O事件,如上圖所示。兩者不同的是,在等待數據達到期間,I/O複用是會阻塞應用程序,而SIGIO方式是不會阻塞應用程序的。
異步I/O
POSIX規範定義了一組異步操作I/O的接口,不用關心fd 是阻塞還是非阻塞,異步I/O是由內核接管應用層對fd的I/O操作。異步I/O嚮應用層通知I/O操作完成的事件,這與前面介紹的I/O 複用模型、SIGIO模型通知事件就緒的方式明顯不同。以aio_read 實現異步讀取IO數據爲例,如圖5所示,在等待I/O操作完成期間,不會阻塞應用程序。
目前常見的服務端模型(多進程結合I/O多路複用)
僞代碼:
void dispatch(...){
將connect_fd加入epoll隊列,等待可讀(可寫)事件
}
void run(...){
執行業務邏輯(read或者write該fd)
//其他業務邏輯(如curl)
}
int main()
{
創建listen_fd監聽端口
fork創建子進程
if(當前進程爲父進程){
/*************父進程***************/
管理子進程
}else{
/*************子進程***************/
子進程創建epoll隊列
將dispatch事件綁定到listen_fd的可讀事件上
使用epoll函數監聽listen_fd
while(1){
//第一次觸發epoll函數
出現listen_fd可讀(可寫)事件,觸發dispatch事件
epoll隊列中有listen_fd和每個客戶端的connect_fd
//第N次(N!=1)
出現listen_fd或者connect_fd的可讀(可寫)事件,觸發對應的dispatch事件或者run事件
}
}
}
從服務器端的代碼邏輯可以知道,epoll是可以同時監聽多個fd的,並且在有對應的事件時才喚醒對應的事件函數,這是通常說的異步調用。
但是,這裏有個問題,如果在run函數中業務邏輯需要創建tcp連接(如curl)請求其他服務接口(創建fd),此時的fd因爲沒有使用epoll,就會出現阻塞。
那我們設想一下,如果在run函數中寫epoll監聽該fd,那如果這個tcp連接請求的後續事件依舊需要創建tcp請求呢,這個代碼該怎麼編寫下去呢,這就是常說的一種情況,回調地獄。
那如何優雅解決這個問題呢?
設想一下,如果我們不需要去寫epoll監聽,直接對read/write函數進行hook,默認所有IO操作行爲會被直接加入epoll隊列,這樣就不會有回調地獄。因此,協程誕生了!
協程
協程是一種程序組件,是由子例程(過程、函數、例程、方法、子程序)的概念泛化而來的,子例程只有一個入口點且只返回一次,而協程允許多個入口點,可以在指定位置掛起和恢復執行。協程是一種“僞多線程”,一個線程中可以包含多個協程,但同一時刻只能有一個協程在運行。
協程是一種可以暫停執行過程的函數,它可以中斷當前的執行過程直到下一個Yield指令達成。在實現上,大多數都是以函數來作爲一個協程,因此這裏列出此簡化的定義方便理解。
例如:實現一個0到9的循環輸出。
int function(void) {
static int i, state = 0;
switch (state) {
case 0: /* start of function */
for (i = 0; i < 10; i++) {
state = __LINE__ + 2; /* so we will come back to "case __LINE__" */
return i;
case __LINE__:; /* resume control straight after the return */
}
}
}
用static變量保存上下文,再用switch進行代碼行的跳轉,就可以實現一個簡單的協程。
協程的運用
總結
協程主要適合於一些IO比較頻繁的系統,在這樣的系統中,使用協程跟多線程的優缺點比較如下:
- 單線程異步IO: 優點是性能高,代碼執行是順序的,不需要關心鎖,競爭等情況;缺點是需要自己處理異步I/O、epoll等,無法便捷地做到hook所有fd操作;
- 協程: 比單線程異步I/O容易編程,代碼更好寫,協程裏面是順序編程的,但協程之間是獨立棧,共享堆內存,單線程執行環境,在一個CPU上運行。協程切換代價比線程少多了,只需要十幾條彙編指令切換寄存器。每秒據說能達到上百萬次切換。
- 多線程同步I/O: 代碼相對也好寫,跟協程一樣獨立棧,共享堆內存。 需要處理資源競爭問題,而且線程切換代價特別大,linux裏面沒有原生的線程,是用進程實現的。
- 多進程:代碼相對容易編寫,但需要解決共享數據的問題。