基於彙編的 C/C++ 協程 - 背景知識

近幾年來,協程在 C/C++ 服務器中的解決方案開始涌現。本文主要闡述以彙編實現上下文切換的協程方案,而且說明其在異步開發模式中的應用。python

本文地址:http://www.javashuo.com/article/p-zzjqpbzk-eq.html編程

首先,咱們來看一下 C/C++ 服務器開發的歷史。segmentfault

參考資料

傳統的 C/C++ 服務器設計框架

同步 I/O 框架

長期以來,使用 C/C++ 編寫服務器程序的時候,每每使用的是多進程模式:一個父進程負責 accept 傳入鏈接,而後 fork 一個子進程處理;或者是一個父進程建立了一個 socket 以後,fork 出多個子進程同時執行 accept 和處理。服務器

爲何說這是同步呢?由於這個設計思路,徹底就是教科書般的、對於 socket 處理的思路。這個思路我在我關於 libev 的介紹文後的評論中也說起:併發

  • 科班出身的軟件專業每每會簡單學到 socket, bind, connect, accept, read, write 等等一路下來的 API。正好,若是真的是按順序這樣調用下來,而後寫了一個 server / client 的話,能夠算是一種同步 I/O 的編程思路。

程序執行的每一步系統調用,都會阻塞住(直接的結果就是致使進程切換),等待遠端機器的響應,而且直到數據到達以後,纔會執行下一步。這就是典型的同步(阻塞) I/O。框架

上面的每步流程若是簡單寫下來的話,支撐不起高併發,由於阻塞的存在。爲了解決這個問題,加入 fork,就能夠實現對多個客戶端的服務了。異步

同步開發模式

同步 I/O 框架,使用的是同步開發模式。人的思惟,是同步化的,先作什麼後作什麼,都是一條線式的流程。這樣一條線從頭至尾的開發模式,就是同步開發模式,很是符合人的思路習慣,便於設計、理解。socket

同步 I/O 的優點

  1. 簡單、一目瞭然——同步 I/O 框架中,使用同步開發模式,所以設計出來的程序代碼簡潔、明確。
  2. bug 少——其實這上面已經講了,程序簡潔、明確、易於理解,也就容易 debug 了。
  3. 柔性高——一個服務以一個進程的模式存在,若是程序有 bug,崩潰了,也不會影響到其餘的服務。此外,若是子進程是處理完鏈接就直接 exit 退出的話,那麼幾乎不用考慮內存泄露的問題——進程建立的全部資源都會被操做系統回收。

同步 I/O 的劣勢

  1. 效率低——fork 設計進程間切換,這是一個須要陷入內核的操做,耗時長,對於高併發場景,對服務器資源的利用效率很低。
  2. 進程間同步複雜——進程何時佔用 CPU、何時被切換掉,這是沒法預料的,所以進程間須要作好同步。多進程同步的 debug 是很是很是複雜的。
  3. 進程間通訊複雜——這個沒什麼好說的,進程間通訊,夠寫一本書了。這一點,在各任務之間還須要通訊的場景中,反而加大了開發複雜度。

吐槽一下,本人進入工做後就見到的第一個服務器就是基於 libevent 設計的,而且整個團隊都一直這麼設計,以致於我曾經覺得同步 I/O 根本沒人用……函數


異步 I/O 框架

首先講從技術層面的 「異步 I/O 框架」 是怎麼回事。維基百科上對 「異步 IO」 的定義是:高併發

  • 發起IO請求的線程不等IO操做完成,就繼續執行隨後的代碼,IO結果用其餘方式通知發起IO請求的程序。

也就是說,某個進程 or 線程,須要告訴操做系統:我須要在某個文件描述符或句柄上 readwrite,可是進程 or 線程並不等待 readwrite ready,而是等到真正有數據可讀或可寫入數據的時候,再執行相應的操做。

其實 read / write 一早就在理論上對這樣的操做提供了支持,那就是 O_NONBLOCK 標誌。當對 socket 設置了該標誌後,若是執行 read / write,資源暫時不可用的話,會返回響應的錯誤。此時,程序就能夠跳過這個句柄,去查看下一個資源了。

但這個方案顯然是不現實的,由於當客戶端數量很大的時候,對全部的資源都須要進行輪詢操做,這是對 CPU 時間的極大浪費,也極大地拉低了服務的響應速度。所以,操做系統須要提供定義的後半段:「通知」。

實現 「通知」 的辦法,其實就是一個系統調用:select。其實 select 的效率很低,通常操做系統會提供替代。對於 Linux 而言,就是 epoll。關於異步 I/O 原理和編程,個人文章有不少了,能夠點擊這裏查看。

從技術層面上,異步 I/O 框架有如下的優點:

  • 效率高——判斷那個資源上的事件 ready,至少是 O(NlogN) 的複雜度,效率極高
  • 單線程多任務——單一一個線程就能夠處理多個傳入請求,達到僞並行的效果,沒有進程/線程切換,大大提升了處理速度,優化了 CPU 使用率
  • 同一線程中沒有同步問題——同一線程的多任務若是須要相互通訊,那麼徹底沒有競爭和同步的問題

然而,單線程多任務其實也是很大的一個劣勢——多個任務都在一個線程 / 進程中處理,若是程序有 bug,那麼整個進程都會崩潰,這對服務器的開發質量要求很高。

異步開發模式

異步 I/O 框架,大部分使用的就是異步開發模式。咱們先不用這個詞彙吧,換成你們比較熟悉的詞。下面兩個詞,其實均可以解釋什麼叫異步開發模式:

  1. 基於事件驅動的開發模式
  2. 狀態機編程

異步開發模式它是基於事件驅動的,當什麼事件到來,就調用哪一個回調進行處理——或者是回調判斷髮生了什麼事件,再調用不一樣的函數處理。這與咱們傳統的思惟不一樣,所以很大程度上,咱們須要畫狀態機,才能很好地解釋咱們的軟件邏輯。

異步開發模式的缺點

其實,異步開發的世界中,盡是各類回調以及回調的註冊。若是咱們不是相應的業務代碼的開發者,那麼走讀代碼時,看到一段函數執行完後,咱們根本不知道這段函數的調用方是誰,從而也就沒法跟蹤判斷下一段代碼是什麼。

這就給調試帶來了極大的困難。其實即使是程序的開發者,若是文檔不足的話,當時間長了以後,恐怕也會忘記本身當時的業務邏輯了吧……所以,異步開發模式對開發者的水平和團隊編程風格的要求很高。

異步開發模式的優勢

可是異步開發模式也有很大的優勢,那就是狀態機編程。這其實很好理解,對於那種邏輯並非一整條簡單的直線,而是有着很是多的分叉——有不少外部觸發條件、而且會致使不少不一樣狀態切換的程序而言,異步開發模式簡直是福音。

示例

好比電梯,一個正運行中的電梯,其執行邏輯很容易被某一樓層用戶按下按鈕這一動做中斷。電梯須要對用戶的操做進行及時的響應,以決定本身接下來應該採起什麼操做。

一個電梯,至少有如下幾個階段,中斷可能發生在電梯運行中的任何一個階段:

  • 電梯靜止,門已經關閉
  • 電梯靜止,門已經打開
  • 電梯靜止,門正在打開
  • 電梯靜止,門正在關閉
  • 電梯勻速運行
  • 電梯加速運行
  • 電梯減速運行
  • 電梯故障異常
  • 電梯檢修

此外,中斷的類型還多是多種多樣:

  • 管理員直接下達指令操做
  • 同樓層用戶按下
  • 不一樣樓層用戶按下

若是使用同步開發模式,這樣的邏輯簡直是災難!

協程

前文我刻意將同步開發模式和同步 I/O、異步開發模式和異步 I/O 分開來講明。確實,開發模式和技術手段是兩碼事。在邏輯比較線性的(相比起上面 「電梯」 的例子)服務(特別是海量服務)而言,咱們最理想的開發方案就是:

  1. 使用同步開發模式——最適合人腦的思惟方式,同時也便於進行程序的調試和 debug
  2. 使用異步 I/O 技術——效率最高的底層實現

曾經我覺得這二者的結合在 C/C++ 上是沒法實現的,直到我換了東家以後才知道,原來能夠這麼玩——

協程簡介

協程,做爲一種服務器組件,在多種高級語言中存在。相比起線程和進程而言,它的切換很是速度快(不用陷入內核態,沒有系統調用),很適合在海量服務中使用。

可是在以 C/C++ 爲主的中級語言服務器開發中,一直沒有大規模引入。緣由是,C/C++ 實在是太接近底層了,彙編後的目標文件,直接就是彙編語言代碼;而彙編語言的下面,則直接就是虛擬內存了,可以對其施加影響的,只有操做着更加底層(硬件寄存器)的操做系統。

可是其餘高級語言不一樣,好比 Java。Java 在原理上是解釋型語言,可是從開發者的角度,其實和編譯型語言無異,只是它把代碼編譯成了由 JVM 能夠識別的程序罷了。這樣,在真正執行的程序(二進制代碼)和程序代碼之間,JVM 能夠提供一箇中間層——以往由操做系統執行的任務調度和上下文切換,JVM 能夠接管過來,在用戶態中完成。這就是協程的實現。

協程原理

協程的實現,涉及兩個內容:

  1. 協程調度
  2. 上下文切換

C/C++ 協程的調度

協程調度的原理,往大了說,其實和線程 / 進程的調度原理無異。這裏分搶佔和非搶佔兩種了。對於 C/C++ 而言。

要實現搶佔式很難,並且也沒太大必要,由於花了很大力氣實現搶佔式的協程調度,反而失去了前文提到的 「同一線程中沒有同步問題」 這一優點了。

因此,針對 C/C++ 協程,最好的方式就是使用非搶佔式調度,須要任務經過某些調用主動讓出 CPU 使用權。再進一步具體化到服務器編程中,因爲每個合法的傳入鏈接的優先級是相同的,所以只須要使用基於 epoll 的實現來進行簡單調度就好了。

基於彙編實現的 C/C++ 協程的上下文切換

上下文切換,是 C/C++ 協程的一大難題,這也是致使了 C/C++ 長期沒有可用的、統一的協程庫的緣由。這一部分行文比較長,我仍是放在下一篇文章裏面講吧。

相關文章
相關標籤/搜索