近幾年來,協程在 C/C++ 服務器中的解決方案開始涌現。本文主要闡述以彙編實現上下文切換的協程方案,而且說明其在異步開發模式中的應用。python
本文地址:http://www.javashuo.com/article/p-zzjqpbzk-eq.html編程
首先,咱們來看一下 C/C++ 服務器開發的歷史。segmentfault
長期以來,使用 C/C++ 編寫服務器程序的時候,每每使用的是多進程模式:一個父進程負責 accept
傳入鏈接,而後 fork
一個子進程處理;或者是一個父進程建立了一個 socket
以後,fork
出多個子進程同時執行 accept
和處理。服務器
爲何說這是同步呢?由於這個設計思路,徹底就是教科書般的、對於 socket 處理的思路。這個思路我在我關於 libev 的介紹文後的評論中也說起:併發
程序執行的每一步系統調用,都會阻塞住(直接的結果就是致使進程切換),等待遠端機器的響應,而且直到數據到達以後,纔會執行下一步。這就是典型的同步(阻塞) I/O。框架
上面的每步流程若是簡單寫下來的話,支撐不起高併發,由於阻塞的存在。爲了解決這個問題,加入 fork
,就能夠實現對多個客戶端的服務了。異步
同步 I/O 框架,使用的是同步開發模式。人的思惟,是同步化的,先作什麼後作什麼,都是一條線式的流程。這樣一條線從頭至尾的開發模式,就是同步開發模式,很是符合人的思路習慣,便於設計、理解。socket
exit
退出的話,那麼幾乎不用考慮內存泄露的問題——進程建立的全部資源都會被操做系統回收。fork
設計進程間切換,這是一個須要陷入內核的操做,耗時長,對於高併發場景,對服務器資源的利用效率很低。吐槽一下,本人進入工做後就見到的第一個服務器就是基於 libevent 設計的,而且整個團隊都一直這麼設計,以致於我曾經覺得同步 I/O 根本沒人用……函數
首先講從技術層面的 「異步 I/O 框架」 是怎麼回事。維基百科上對 「異步 IO」 的定義是:高併發
也就是說,某個進程 or 線程,須要告訴操做系統:我須要在某個文件描述符或句柄上 read
或 write
,可是進程 or 線程並不等待 read
或 write
ready,而是等到真正有數據可讀或可寫入數據的時候,再執行相應的操做。
其實 read
/ write
一早就在理論上對這樣的操做提供了支持,那就是 O_NONBLOCK
標誌。當對 socket 設置了該標誌後,若是執行 read / write,資源暫時不可用的話,會返回響應的錯誤。此時,程序就能夠跳過這個句柄,去查看下一個資源了。
但這個方案顯然是不現實的,由於當客戶端數量很大的時候,對全部的資源都須要進行輪詢操做,這是對 CPU 時間的極大浪費,也極大地拉低了服務的響應速度。所以,操做系統須要提供定義的後半段:「通知」。
實現 「通知」 的辦法,其實就是一個系統調用:select
。其實 select 的效率很低,通常操做系統會提供替代。對於 Linux 而言,就是 epoll
。關於異步 I/O 原理和編程,個人文章有不少了,能夠點擊這裏查看。
從技術層面上,異步 I/O 框架有如下的優點:
然而,單線程多任務其實也是很大的一個劣勢——多個任務都在一個線程 / 進程中處理,若是程序有 bug,那麼整個進程都會崩潰,這對服務器的開發質量要求很高。
異步 I/O 框架,大部分使用的就是異步開發模式。咱們先不用這個詞彙吧,換成你們比較熟悉的詞。下面兩個詞,其實均可以解釋什麼叫異步開發模式:
異步開發模式它是基於事件驅動的,當什麼事件到來,就調用哪一個回調進行處理——或者是回調判斷髮生了什麼事件,再調用不一樣的函數處理。這與咱們傳統的思惟不一樣,所以很大程度上,咱們須要畫狀態機,才能很好地解釋咱們的軟件邏輯。
其實,異步開發的世界中,盡是各類回調以及回調的註冊。若是咱們不是相應的業務代碼的開發者,那麼走讀代碼時,看到一段函數執行完後,咱們根本不知道這段函數的調用方是誰,從而也就沒法跟蹤判斷下一段代碼是什麼。
這就給調試帶來了極大的困難。其實即使是程序的開發者,若是文檔不足的話,當時間長了以後,恐怕也會忘記本身當時的業務邏輯了吧……所以,異步開發模式對開發者的水平和團隊編程風格的要求很高。
可是異步開發模式也有很大的優勢,那就是狀態機編程。這其實很好理解,對於那種邏輯並非一整條簡單的直線,而是有着很是多的分叉——有不少外部觸發條件、而且會致使不少不一樣狀態切換的程序而言,異步開發模式簡直是福音。
好比電梯,一個正運行中的電梯,其執行邏輯很容易被某一樓層用戶按下按鈕這一動做中斷。電梯須要對用戶的操做進行及時的響應,以決定本身接下來應該採起什麼操做。
一個電梯,至少有如下幾個階段,中斷可能發生在電梯運行中的任何一個階段:
此外,中斷的類型還多是多種多樣:
若是使用同步開發模式,這樣的邏輯簡直是災難!
前文我刻意將同步開發模式和同步 I/O、異步開發模式和異步 I/O 分開來講明。確實,開發模式和技術手段是兩碼事。在邏輯比較線性的(相比起上面 「電梯」 的例子)服務(特別是海量服務)而言,咱們最理想的開發方案就是:
曾經我覺得這二者的結合在 C/C++ 上是沒法實現的,直到我換了東家以後才知道,原來能夠這麼玩——
協程,做爲一種服務器組件,在多種高級語言中存在。相比起線程和進程而言,它的切換很是速度快(不用陷入內核態,沒有系統調用),很適合在海量服務中使用。
可是在以 C/C++ 爲主的中級語言服務器開發中,一直沒有大規模引入。緣由是,C/C++ 實在是太接近底層了,彙編後的目標文件,直接就是彙編語言代碼;而彙編語言的下面,則直接就是虛擬內存了,可以對其施加影響的,只有操做着更加底層(硬件寄存器)的操做系統。
可是其餘高級語言不一樣,好比 Java
。Java 在原理上是解釋型語言,可是從開發者的角度,其實和編譯型語言無異,只是它把代碼編譯成了由 JVM
能夠識別的程序罷了。這樣,在真正執行的程序(二進制代碼)和程序代碼之間,JVM 能夠提供一箇中間層——以往由操做系統執行的任務調度和上下文切換,JVM 能夠接管過來,在用戶態中完成。這就是協程的實現。
協程的實現,涉及兩個內容:
協程調度的原理,往大了說,其實和線程 / 進程的調度原理無異。這裏分搶佔和非搶佔兩種了。對於 C/C++ 而言。
要實現搶佔式很難,並且也沒太大必要,由於花了很大力氣實現搶佔式的協程調度,反而失去了前文提到的 「同一線程中沒有同步問題」 這一優點了。
因此,針對 C/C++ 協程,最好的方式就是使用非搶佔式調度,須要任務經過某些調用主動讓出 CPU 使用權。再進一步具體化到服務器編程中,因爲每個合法的傳入鏈接的優先級是相同的,所以只須要使用基於 epoll
的實現來進行簡單調度就好了。
上下文切換,是 C/C++ 協程的一大難題,這也是致使了 C/C++ 長期沒有可用的、統一的協程庫的緣由。這一部分行文比較長,我仍是放在下一篇文章裏面講吧。