協程和IO多路復用


協程

在這里插入圖片描述

我們已經知道線程是進程中的執行體,擁有一個執行入口,以及從進程虛擬地址空間中分配的棧(用戶棧和內核棧),操作系統會記錄線程控制信息,而線程獲得CPU時間片以后才可以執行,CPU這里棧指針,指令指針,棧基等寄存器都要切換到對應的線程。

在這里插入圖片描述

如果線程自己又創建了幾個執行體(攜程),給它們各自指定執行入口,申請一些內存分給它們用作執行棧,那么線程就可以按需調度這幾個執行體了。
在這里插入圖片描述
為了實現這些執行體的切換,線程也需要記錄它們的控制信息。包括ID,執行棧的位置,執行入口地址,執行現場等等。線程可以選擇一個執行體來執行,此時CPU中指令指針就會指向這個執行體的執行入口,棧基和棧指針寄存器也會指向線程給它分配的執行棧。

在這里插入圖片描述
要切換執行體時,需要先保存當前執行體的執行現場,然后切換到另一個執行體
在這里插入圖片描述
通過同樣的方式,可以恢復到之前的執行體, 這樣就可以從上次中斷的地方繼續執行。這些由線程創建的執行體就是所謂的“攜程”

在這里插入圖片描述
因為用戶程序不能操作內核空間,所以只能給協程分配用戶棧,而操作系統對協程一無所知,所以協程又被稱為“用戶態線程”。

在這里插入圖片描述

協程的思想很早就被提出來了,最初是為了解決編譯器實現中的問題,后來相繼出現了很多種實現方式,例如windows中的纖程,再例如lua中的coroutine。

在這里插入圖片描述
可無論被賦予什么樣的名字,有着怎樣的用法,在創建協程時,都要指定執行入口,底層都會分配協程執行棧和控制信息。否則又該如何實現用戶態調度呢?而讓出執行權時,也都要保存執行現場,不然如何能夠從中斷處恢復執行呢?
在這里插入圖片描述
所以協程思想的關鍵在於,控制流的“主動讓出”和“恢復”,每個協程都擁有自己的執行棧,可以保存自己的執行現場。

在這里插入圖片描述

所以可以由用戶程序,按需創建協程,協程“主動讓出”執行權時,會保存執行現場,然后切換到其它協程,協程恢復執行時,會根據之前保存的執行現場,恢復到中斷前的狀態繼續執行,這樣就通過協程,實現了既輕量又靈活的,由用戶態進行調度的,多任務模型。

在這里插入圖片描述
即便如此,協程依然風平浪靜很多年

在這里插入圖片描述
直到高並發成為主流趨勢,瞬間抵達的海量請求讓多進程模型下內存資源捉襟見肘

在這里插入圖片描述
讓多線程模型下,內核態用戶態兩頭忙,卻依然疲於應對。

在這里插入圖片描述

協程這種靈活,輕量的用戶態調度模型,便受到了廣泛的關注。而真正讓協程大放異彩的,是它在IO多路復用中的應用,二者的結合,助力協程成為炙手可熱的高並發解決方案。

IO多路復用

前言

待。。。
我們知道通過操作系統記錄的進程控制信息,可以找到打開文件描述符表,進程打開的文件,創建的socket等等,都會記錄到這張表里。

在這里插入圖片描述
socket的所有操作都由操作系統來提供,也就是要通過系統調用來完成,每創建一個socket,就會在打開文件描述符表中,對應增加一條記錄,而返回給應用程序的只有一個socket描述符,用於識別不同的socket。

在這里插入圖片描述
而且每個TCP socket在創建時,操作系統都會為它分配一個讀緩沖區和一個寫緩沖區,要獲得響應數據,就要從讀緩沖區拷貝過來,同樣的要通過socket發送數據,也要先把數據拷貝到寫緩沖區才行。

在這里插入圖片描述
所以,問題出現了,用戶程序想要讀數據的時候,讀緩沖區里未必有數據,想發送數據的時候,寫緩沖區里也未必有空間。

在這里插入圖片描述
那怎么辦?第一種辦法,乖乖的讓出CPU,進到等待隊列里,等socket就緒后,再次獲得時間片就可以繼續執行了。這就是阻塞式IO。

在這里插入圖片描述
使用阻塞式IO,要處理一個socket就要占用一個線程。等這個socket處理完才能接手下一個,這在高並發場景下會加劇調度開銷

在這里插入圖片描述
第二中辦法是非阻塞式IO,也就是不讓出CPU,但是需要頻繁的檢查socket是否就緒了。這是一種“忙等待”的方式,很難把握輪詢的間隔時間,容易造成空耗CPU,加劇相應延遲。

在這里插入圖片描述
第三種辦法就是“IO多路復用”,由操作系統提供支持,把需要等待的socket加入到監聽集合,這樣就可以通過一次系統調用,同時監聽多個socket。

在這里插入圖片描述
有socket就緒了,就可以逐個處理了,既不用為了等待某個socket而阻塞,也不會陷入“忙等待”之中

Linux中提供了三種IO多路復用的實現方式

select

在這里插入圖片描述
第一種select,我們可以設置要監聽的描述符,也可以設置等待超時時間,如果有准備好的fd,或達到指定超時時間,select函數就會返回。從函數簽名來看,它支持監聽可讀,可寫,異常三類事件。因為這個fd_set是個unsigned long型的數組,共16個元素,每一位對應一個fd,16*64=1024,最多可以監聽1024個fd。這就有點少了,而且每次調用select都要傳遞所有監聽集合,這就需要頻繁的從用戶態到內核態拷貝數據。除此之外,即便有fd就緒了,也需要遍歷整個監聽集合,來判斷哪個fd是可操作的,這些都會影響性能。

poll

在這里插入圖片描述
第二種IO多路復用實現方式:poll。雖然支持的fd數目,等於最多可打開的文件描述符的個數,但是另外兩個問題依然存在。

epoll

在這里插入圖片描述
而epoll就沒有這些問題了,它提供三個接口,epoll_create1用於創建一個epoll,並獲取一個句柄,epoll_ctl用於添加,修改或刪除fd與對應的事件信息,除了指定fd和要監聽的事件類型,還可以傳入一個event data,通常會按需定義一個數據結構,用於處理對應的fd。可以看到每次都只需傳入要操作的一個fd,無需傳入所有監聽集合,而且只需要注冊這一次,通過epoll_wait得到的fd集合都是已經就緒的,逐個處理即可,無需遍歷所有監聽集合。

使用協程

在這里插入圖片描述
通過IO多路復用,線程再也不用為了等待某一個socket,而阻塞或空耗CPU。並發處理能力因而大幅提升,但是也並非沒有問題,例如一個socket可讀了,但是這回只讀到了半條請求,也就是說需要再次等待這個socket可讀,在繼續處理下一個socket之前,需要記錄下這個socket的處理狀態,下一個這個socket可讀時,也需要恢復上次保存的現場,才好繼續處理。

也就是說,在IO多路復用中實現業務邏輯時,我們需要隨着事件的等待和就緒,而頻繁的保存和恢復現場,這並不符合常規開發習慣,如果業務邏輯比較簡單還好,若是較為復雜的業務場景,就是悲劇了。

在這里插入圖片描述
既然業務處理過程中,要等待事件時,需要保存現場並切換到下一個就緒的fd,而事件就緒時又需要恢復現場繼續處理,那豈不是很適合協程?
在這里插入圖片描述

在IO多路復用這里,事件循環依然存在,依然要在循環中逐個處理就緒的fd,但處理過程卻不是圍繞具體業務,而是面向協程調度。如果是用於監聽端口的fd就緒了,就建立連接創建一個新的fd,交給一個協程來負責,協程執行入口就指向業務處理函數入口,業務處理過程中,需要等待時就注冊IO事件,然后讓出。

在這里插入圖片描述
這樣執行權就會回到切換到該協程的地方繼續執行。如果是其他等待IO事件的fd就緒了,只需要恢復關聯的協程即可。

在這里插入圖片描述
協程擁有自己的棧,要保存和恢復現場都很容易實現。這樣IO多路復用這一層的事件循環,就和具體業務邏輯解耦了。可以把read,write,connect等可以回發生等待的函數包裝一下,在其中實現IO事件注冊與主動讓出,這樣在業務邏輯層面就可以使用這些包裝函數,按照常規的順序編程方式,來實現業務邏輯了。

在這里插入圖片描述
這些包裝函數在需要等待時,就會注冊IO事件,然后讓出協程。這樣我們在實現業務邏輯時就完全不用關心保存和恢復現場的問題了。協程和IO多路復用之間的合作,不僅保留了IO多路復用的高並發性能,還解放了業務邏輯的實現。
在這里插入圖片描述
其實在Golang,OpenResty,Swoole中,協程和IO多路復用的合作方式,核心思想大抵如此。
在這里插入圖片描述
為了增強理解,我們按照這個思路實現了一個精簡版的實例,項目結構是這樣的,最底層實現了協程,定義了協程對應的數據結構,提供協程初始化與切換等核心功能。基於這些功能實現了協程池,可以預創建一批協程備用,回收空閑協程復用,還可以根據請求情況縮減或擴張協程池規模,這樣上層應用就不用關心協程的實現細節了。協程池之上是IO多路復用功能的實現,目前已實現對這些IO多路復用的支持,對於學習理解相關概念而言足夠了。再往上是框架層,封裝了事件循環,實現了基於IO的協程調度,有了框架層的封裝,我們只需要關注業務層面的具體實現就ok了。

完整代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM