聊聊協程


在這裏插入圖片描述

引言

還記得大概是去年十一月份的時候,心中萌發了用協程去優化下寫的http服務器這一想法,但是協程是什麼?爲什麼可以優化?一概不清楚,不明白。還記得當時大概花了一晚上的時間,去搜相關的知識點,得到的結果甚至是比搜之前更爲迷惑,究其原因還是基礎薄弱是一點,其次且就是相關資料的欠缺,大多都是你拷我,我拷你,千人一面。現在經過一段時間對於協程的學習,算是入了個門,對於這麼個東西也是有了一些自己的想法,遂希望記錄下來,能給需要的人一點幫助。

協程是什麼

協程的概念最早在1963年就被提出,這是早於線程的,但是一般來說還是線程更加廣爲人知,我覺得着這還是有其合理性的,追本溯源,協程是一種協作式多任務模型,而線程是一種搶佔式多任務的模型。我們知道最早的時候進程也是一種協作式多任務(也叫非搶佔式)的模型,什麼是協作式多任務?假如我們有兩個進程一個CPU,進程A先佔有CPU,當它執行完或希望主動讓出CPU的時候就會主動執行切換,這個時候進程B纔可以拿到執行權,這種方法顯然是有很大的弊端,同一臺計算機上可能運行着很多的程序,這些程序的編寫者中不見得都是心懷好意,當一個進程陷入死循環的時候其他進程無法得到CPU執行權,只能眼巴巴的看着,這是因爲進程A即沒有結束也沒有主動讓出CPU。

相比之下,我們所熟知的搶佔式多任務(搶佔式調度)就優秀的多,這裏面的代表調度算法就是時間片輪轉調度算法,每個進程給你一個時間片,不管你是誰,執行完了這個時間片也得切換,針對於高優先級的任務只需要在調度的時候優先分配時間片就可以了。這種方法巧妙的避免用戶進程中個別的“壞傢伙”,使得整個OS可以在有惡意進程的時候仍正常運行。

隨着計算機的發展,各種工具也都變得越來越複雜,比如在線觀看視屏,這顯然至少需要兩個進程,一個負責下載,一個負責播放,兩個進程間的交互不是個小問題,我們不但需要定義兩個進程共享文件的格式,還需要對這個共享的文件加鎖,這也就是我們所熟知的RPC,但本不必這樣,兩個進程乾的事情完全可以合在一起,爲了效率的提升,線程誕生了,當然也繼承了進程搶佔式的血統。這個時候原先的兩個進程變成了一個進程的兩個線程,共享頁表,可以以幾乎忽略不計的代價進行通信了,但是問題也隨之而來,即data race,也就是我們通常所說的條件競爭,仔細一想也很好理解,兩個線程分別運行在兩個CPU上,並在同一個時間點執行同一條指令,這就會產生數據競爭,解決的方法就是加鎖,這伴隨着加鎖解鎖兩個用戶態轉向內核態的過程,但是確實解決了我們的問題。這個時候非搶佔式的好處就體現出來了,因爲協程A很清楚我什麼時候可以把數據用完,當協程A用完的時候把CPU交給協程B,協程B再去執行,這實際是一個串行的過程,當然就省去了線程上我們做的那些努力。

再來說說協程,協程聽名字就知道它是一個協作式多任務的模型。其實協程本質上就是一個用戶態的線程,實現一個協程庫也實際就是去實現一個用戶態的調度器,也就是說對於內核來說,如果我們在一個線程內申請了10個協程,它眼裏的始終只是一個線程不停的在跑,而在我們用戶眼中,十個不同的“程序”在不停的切換執行,確實十分神奇,也看似十分高效,我們不妨來思考一下它到底在哪些地方提升了效率。

哪裏提升了效率?

以上面的一個線程十個協程舉例,內核看來只有一個線程在跑,極其顯然,這些協程是在串行,而不是並行,儘管你有十個協程,但你也只能使用一個CPU。所以想要高效的利用CPU我們還是需要使用線程。那麼協程究竟快在哪裏?我想最大的優勢就是協程的切換相比於線程的切換更快,我們知道在線程執行一些阻塞的系統調用的時候線程會從執行態轉化爲阻塞態,伴隨着內核態到用戶態的轉化,這一過程有以下幾個消耗資源的地方:

  1. 上下文切換(本質就是寄存器切換)。
  2. 特權模式切換。(調度算法中的動態特權級)
  3. 而且內核代碼對用戶不信任,需要進行額外的檢查。

而協程的切換就只需要執行一個上下文的切換,且全部的操作都在用戶空間完成,當然這裏效率的提升有提升,但肯定不是量級的。第二個效率提升的地方在於使得兩個互相依賴的代碼不需要再去考慮data race,省去了維護同步所帶來的開銷,這個開銷不得不說是非常可觀的,光是加鎖解鎖就要進行兩次內核態到用戶態的轉換。

適用的場景

協程只是一種解決問題的方式,且不是一種通用方式,它只是爲我們提供了有一種新的運行時抽象,而這種抽象可以在某些場景被完美的契合。那麼協程的適用場景是什麼呢?答案就是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因爲IO的速度遠低於CPU,緩存,內存的速度)。在學習多線程編程的時候我們知道,當任務爲IO密集型任務時,我們需要把線程數設置爲大於CPU物理核數的一個值,因爲在IO密集型任務執行期間,99%的時間都花在IO上,花在CPU上的時間很少。爲了充分的利用CPU,我們需要大量的線程,而這意味着大量的線程切換,這一點我們上面已經討論過了,這種情況下顯然協程可以提升我們程序的效率。

當然我們在編寫協程代碼的時候時刻腦子裏要清楚一件事情,在一個線程上,無論你開了多少協程,始終只是一個CPU在跑罷了。所以一般情況下我們的模型是多線程搭配多協程或者多進程搭配多協程,做到充分的利用CPU。

協程的實現原理

說了那麼多,你一定對這個東西充滿了疑惑,到底怎麼才能做到在一個線程上還能執行十個協程,且這些協程的還有各自的不同的代碼段呢?我上面提到了一點,實現一個協程庫實際上就是實現一個用戶態的“線程”調度器,因爲我們知道程序的執行依賴的是寄存器上的值,包括我們熟知的PC,ESP,EBP,CS,DS,FLAGS等等,所以上下文的切換實際上就是把寄存器的值進行切換,但是什麼時候切換呢?我們知道一般來說協程庫爲我們的會提供以下兩個函數,一個是resume,功能爲執行一個指定協程,一個是yeild,功能爲切換CPU執行權到另一個協程,有了這兩個函數,我們可以隨心所欲的去切換協程了。那麼什麼時候切換呢?答案就是在進行阻塞式系統調用進行切換,當然你可能會問如何做到在執行系統提供給我們的函數時執行我們自己的邏輯呢?這裏有兩個方法,一個是協程庫替換系統調用,這當然比較直接了。還有一種巧妙的方法,騰訊的libco就使用了這種方法,即庫打樁機制,原理爲通過動態鏈接時先加載自己定義的函數已做到把系統提供的函數“hook”掉,使用dlsym系列函數得到原函數地址,執行相關邏輯。

當然以上只是簡單的陳述了一些原理性的東西,實際實現的話還有很多的細枝末節,比如對於協程棧的實現,到底值一個協程一個棧還是共享棧呢?所說的無棧協程到底是怎麼一回事呢?在協程切換出去以後何時切換回來呢?剩下的問題還需要有興趣的同學繼續升入理解。

總結

協程也可以併發的執行多線邏輯,但完全不會給CPU帶來額外負擔,因爲是串行執行,所以不存在任何資源競爭。但是其使用場景是有限制的,比如計算密集型的引用完全沒有必要使用協程,因爲相比於單線程協程的切換也需要不少操作和上下文的切換,讓一個線程不管不顧的計算顯然是一個更優的選擇。至於單機千萬連接,我到現在也認爲連接數是與fd相關的,而fd的改變是靠改內核參數的。一個博主的一句話我覺得很有意思,也送給大家:一個協程就想以一敵百嗎,縱使你有百般能耐,也不可能以一敵百…

參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章