c語言實現的協程

2022-08-01 16:39:14 字數 2251 閱讀 9801

這幾天突然對協程感興趣,於是自己實現了乙個,**放在github上:

協程是一種使用者空間的非搶占式執行緒,主要用來解決等待大量的io操作的問題。

協程vs執行緒

對比使用多執行緒來解決io阻塞任務,使用協程的好處是不用加鎖,訪問共享的資料不用進行同步操作。這裡需要說明的一點是,使用協程之所以不需要加鎖不是因為所有的協程只在乙個執行緒中執行,而是因為協程的非搶占式的特點。也就是說,使用協程的話,在沒主動交出cpu之前都是不會被突然切換到其它協程上的。而執行緒是搶占式的,使用多執行緒你是不能確定你的執行緒什麼時候被作業系統排程,什麼時候被切換,因此需要用鎖到實現一種「原子操作」的語義。

協程vs非同步**

其實更一般更常見的做法是,使用非阻塞的io(比如是非同步io,又或者是在syscall上自己實現的一套非同步io,如asio)並且將處理操作寫在**函式中。這樣的做法一般沒什麼問題,但當**函式變多,一段連貫的業務**就會被拆分到多個**函式之中,增加維護的成本。因此使用協程可以用同步的寫法寫出效果相當於是非同步的**。

利用static變數實現協程

要實現乙個協程,主要的問題是如何儲存函式呼叫的上下文。之前在網上看到一篇部落格coroutines in c,用一種非常簡潔的方式實現了這個儲存上下文的功能。實現**如下:

1

#define crbegin static int _cr_state = 0; switch(_cr_state) while (0)

3#define crfinish }45

intfunc1()

12crfinish

13 }

這個**利用了函式的static變數來儲存函式呼叫狀態。注意,由於vs2013有乙個除錯特性,所以vs2013的__line__的實現不是常量因此會編譯不通過,使用gcc就可以編譯。這段**簡單是簡單但是有問題,比如說如果兩個協程呼叫同乙個函式,就會出錯。因此部落格裡面提及這段**主要是給出乙個思路,如果實際使用的話這樣子肯定是不行的。

利用setjmp、longjmp實現協程

前面說過,實現協程最主要的是儲存函式的呼叫的上下文,而這些上下文主要就兩個部分:1.各個暫存器的值,2.函式呼叫棧。c語言裡可以通過setjmp來儲存函式呼叫時,各暫存器的值。儲存之後,便可以通過longjmp重現回到當初setjmp的地方(可以理解成跨函式的goto)。但是,需要注意的是,setjmp僅負責儲存暫存器的值,不負責維護其函式呼叫棧(這個看看setjmp的jmp_buf的結構就知道了),因此必須由使用者來手動的維護這個函式呼叫棧。使用setjmp、longjmp的乙個常見的錯誤就是,嘗試去longjmp到乙個已經執行完的函式,這時候雖然暫存器的值是當時儲存的值,但是呼叫棧已經不是原來的呼叫棧了。

而我的做法是,在建立乙個協程的時候在堆上申請一塊空間(大小為2m)作為協程的呼叫棧,然後在setjmp的時候,手動更改暫存器esp的值,使其指向這個我自己建立的呼叫棧。因此在以後執行的時候,這個協程就會使用我提供的那塊記憶體作為棧。

我的這個協程庫提供了三個介面:

coro_new:建立乙個協程

coro_yield:將控制權返回給排程協程

coro_main:執行排程協程

協程的控制流程如下:

通過coro_main執行排程協程,並找出下乙個執行的協程,執行之。

執行這個協程直到其呼叫coro_yield將控制權返還給排程協程。

重複以上兩個步驟,直到所有協程執行完畢。

這個協程庫實現的非常簡單,只有100來行的**,當然實現它的目的是為了提供乙個最簡單的協程模型,而不是乙個功能完整、魯棒性強的能投入實際業務執行的協程。

因此問題還是有很多的:

比如當在協程裡面呼叫棧超過2m時,這個是需要處理的,現在的**是沒有做的,理應中斷程式,避免寫壞堆,產生隨機的不可重現的問題。

顯然在實現時沒有考慮到多執行緒,如果在多執行緒環境裡面執行,需要**做同步處理。

現在的這個版本的協程有乙個約定,在協程裡呼叫的函式不能阻塞在syscall,這顯然也是不科學的。乙個完整的協程庫,應該包含一些常用的syscall的非阻塞的實現,畢竟只有乙個執行緒不能真的阻塞在這個呼叫上。

不同平台的jmp_buf實現可能不一致,因此在其它平台上需要稍微改寫**。

總結

當然實現協程還有比較一些更好的方法,比如如果能用glibc的ucontext庫就可以基於這個庫來實現,而不用自己手動管理函式呼叫的上下文了,如雲風實現的協程庫。

參考資料

c語言實現程序的排程 動手實現乙個協程 開篇

學習開發協程的前置知識如下 1 學過c語言 2 學過作業系統中程序的狀態以及程序的排程 3 學過資料結構中的堆 4 學過計算機網路,最好有網路程式設計的經驗 5 學過計算機組成原理,對cpu 記憶體 外存有一定的了解 6 學過組合語言,對常用暫存器有一定的了解 3 學過如何用gdb除錯程式,知道檢視...

C協程實現的效率對比

前段時間實現的c協程依賴棧傳遞引數,在開啟優化時會導致錯誤,於是實現了乙個ucontext的版本,但ucontext的切換效率太差了,在我的機器上執行4000w次切換需要11秒左右,這達不到我的要求,所以重新設計了實現,使得在開啟優化時也能得到正確的結果.並且效率也令人滿意,4000w次切換僅需要7...

gevent實現協程

1 yield實現 import time def task 1 while true print 1 time.sleep 0.1 yield def task 2 while true print 2 time.sleep 0.1 yield def main t1 task 1 建立迭代器 t...