使用ucontext组件实现的coroutine代码分析
coroutine一般翻譯過來就是協程,類似于線程可以切換,而跟線程是由操作系統調度器來實現切換不一樣,協程由用戶程序自己調度進行切換。我以前也看過協程相關的內容,但沒有自己去實現過。最近搞OpenStack,OpenStack各個模塊都是單線程模型,但是用了eventlet的綠色線程,eventlet也是Python的協程實現庫。這篇文章我并不打算剖析Python協程庫的實現,而是分析一個基于Linux下ucontext組件的C語言實現,原作者是云風,我以前也看過這個實現,只是現在忘了,沒有自己寫過或者分析過代碼,只是看看好像永遠是似懂非懂。后來yanyiwu又fork了一個實現并做些修改,據說更易懂,我就直接拿他修改后的版本分析就ok了,這里對他們表示感謝。
這個簡單的實現包含三個文件,分別是頭文件coroutine.h,協程實現文件coroutine.c和測試主程序main.c,我給代碼加了點注釋,并編譯運行。
coroutine.h源碼:
coroutine.h里面是一些宏定義和函數聲明:
coroutine_func:一個函數指針,聲明了coroutine的函數原型;
coroutine_open:要使用該協程庫時第一個被調用的函數,它返回一個調度器結構體;
coroutine_close:關閉協程調度器,最后被調用不解釋;
coroutine_new:將一個函數還有需要傳遞的參數加入到協程的調度器里邊;
coroutine_yield:退出當前運行的協程;
coroutine_resume:恢復具有特定id值的協程;
coroutine_running:返回正在運行的協程id,-1表示沒有正在運行的協程;
schedule_status:返回1表示還有等待運行的協程,返回0表示所有協程都已運行完畢;
主要的實現都在coroutine.c文件,源碼如下:
對coroutine.c源碼我們暫時不作分析,一會兒分析main.c時自然會講到它。
main.c源碼如下:
我們來分析下main.c的代碼。先看下main函數,調用了coroutine_open函數,返回一個調度器結構體,然后調用test函數并把調度器結構體當作參數,最后調用coroutine_close函數關閉調度器。顯然,test函數就是接臟活累活的地方了。看下test函數里的35,36行,調用了coroutine_new創建兩個協程,分別使用了函數foo和foo2,參數分別為arg1和arg2,并返回了協程id,分別為co1和co2。接著是一個while循環,看下代碼:
?
while (schedule_status(S)) {
? ? ? ? ? ? ? ? ?coroutine_resume(S,co1);
? ? ? ? ? ? ? ? ?coroutine_resume(S,co2);
}
可以看出,當schedule_status返回為1時,將對協程co1和co2分別調用?coroutine_resume函數,schedule_status返回0時test函數退出。這回,我們不得不去看coroutine_resume函數了:
coroutine_resume函數有兩個參數,分別為調度器結構體和協程id。該函數首先根據協程id從調度器中獲取對應的協程結構體,然后對狀態status作判斷,可能的狀態為COROUTINE_READY和COROUTINE_SUSPEND。
status為COROUTINE_READY(協程第一次被調度)時:
調用getcontext獲取當前(注意,當前不是傳進來id所對應的協程)協程的上下文,保存在傳進來的id所對應的協程結構體中類型為ucontext_t的變量ctx,接著修改ctx結構體的棧指針和棧大小,并把該協程退出時要執行的協程上下文設置成調度器結構體內類型為ucontext_t的變量main,然后將調度器結構體里running變量設置為要將要執行的協程的id,將要執行的協程的狀態status設置為COROUTINE_RUNNING,再調用makecontext修改要執行協程上下文,參數為要執行的協程上下文變量、mainfunc函數地址、mainfunc參數個數、給mainfunc傳遞的參數,因此后續該協程執行時,就會調用mainfunc函數,最后調用swapcontext,該函數將當前協程的上下文內容保存在調度器結構體的main變量中,并激活要執行的協程上下文,于是mainfunc函數被調用了。
status為COROUTINE_SUSPEND時:
將調度器結構體里的變量running設置成傳進來的參數id,將該id對應的協程狀態status設置成COROUTINE_RUNNING,調用swapcontext保存當前協程上下文,激活執行參數id對應的協程。當協程為這個狀態時,肯定是曾經被調度過了,即經歷過了COROUTINE_READY階段,其棧指針已經被修改過,因此不需要再次修改而直接激活執行。
不難看出,每個協程第一次被調度時,都調用了makecontext函數并把mainfunc函數設置成該協程執行時就去調用的函數,因此我們知道,協程co1和co2所對應函數foo和foo2都是在mainfunc中被調用。我們再看下foo和foo2的實現:
這兩個函數中都有一個for循環,每循環一次就調用coroutine_yield函數,該函數首先將當前協程的狀態status改為COROUTINE_SUSPEND,將調度器結構體里running變量設置為-1,再調用swapcontext將協程上下文保存在當前協程的結構體變量ctx中,激活調度器結構體里main變量對應的協程上下文,這里實際上是切換到了主協程。
說到這里,估計有些同學還是不明不白的,我根據自己的理解具體來解釋一下流程:
while循環里邊對協程co1調用coroutine_resume時,由于第一次調用進入COROUTINE_READY分支,這時候getcontext獲取主協程(不知道描述對不對)的上下文,然后修改棧后作為協程上下文保存在co1對于的協程結構體中,然后mainfunc中執行co1對于的函數foo,在foo中調用了coroutine_yield,這時co1被設置成COROUTINE_SUSPEND,切換到剛才保存的主協程中,這是test函數里邊的coroutine_resume又被調用,不過這時是對co2,同樣的命運,co2對應的foo2被調度執行,沒想到foo2函數也自動將自己設置成COROUTINE_SUSPEND,這時又切換到了主協程中,test中又一次循環開始,coroutine_resume對co1調用,只是這次進入COROUTINE_SUSPEND分支,這回不用設置什么棧了,直接換成執行協程co1,對co2也一樣,不再贅述。
那么問題又來了,co1和co2什么時候徹底結束、主程序得以退出呢?
foo和foo2中的for循環次數是有限的,當循環條件不滿足時,coroutine_yield函數不會被調用,這時mainfunc中的調用:“C->func(S, C->arg); ”結束,之后的語句“C->status = COROUTINE_DEAD;”被調用,將對應的協程狀態status設置為COROUTINE_DEAD。當兩個協程狀態都為COROUTINE_DEAD時,schedule_status函數返回0,主協程中的while循環退出,主程序就退出了。再看下程序的執行結果,一切都變得明了了。
運行結果:
轉載于:https://www.cnblogs.com/woshiweige/p/4518428.html
總結
以上是生活随笔為你收集整理的使用ucontext组件实现的coroutine代码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Tomcat+nginx+keepali
- 下一篇: 微信小程序开发03-这是一个组件