協程是一種使用者态的輕量級線程。本篇主要研究協程的c/c++的實作。
首先我們可以看看有哪些語言已經具備協程語義:
比較重量級的有c#、erlang、golang*
輕量級有python、lua、javascript、ruby
還有函數式的scala、scheme等。
c/c++不直接支援協程語義,但有不少開源的協程庫,如:
目前看到大概有四種實作協程的方式:
第一種:利用glibc 的 ucontext元件(雲風的庫)
第三種:利用c語言文法switch-case的奇淫技巧來實作(protothreads)
本篇主要使用ucontext來實作簡單的協程庫。
利用ucontext提供的四個函數<code>getcontext(),setcontext(),makecontext(),swapcontext()</code>可以在一個程序中實作使用者級的線程切換。
本節我們先來看ucontext實作的一個簡單的例子:
儲存上述代碼到<code>example.c</code>,執行編譯指令:
想想程式運作的結果會是什麼樣?
上面是程式執行的部分輸出,不知道是否和你想得一樣呢?我們可以看到,程式在輸出第一個“hello world"後并沒有退出程式,而是持續不斷的輸出”hello world“。其實是程式通過getcontext先儲存了一個上下文,然後輸出"hello world",在通過setcontext恢複到getcontext的地方,重新執行代碼,是以導緻程式不斷的輸出”hello world“,在我這個菜鳥的眼裡,這簡直就是一個神奇的跳轉。
那麼問題來了,ucontext到底是什麼?
在類system v環境中,在頭檔案< ucontext.h > 中定義了兩個結構類型,<code>mcontext_t</code>和<code>ucontext_t</code>和四個函數<code>getcontext(),setcontext(),makecontext(),swapcontext()</code>.利用它們可以在一個程序中實作使用者級的線程切換。
<code>mcontext_t</code>類型與機器相關,并且不透明.<code>ucontext_t</code>結構體則至少擁有以下幾個域:
當目前上下文(如使用makecontext建立的上下文)運作終止時系統會恢複<code>uc_link</code>指向的上下文;<code>uc_sigmask</code>為該上下文中的阻塞信号集合;<code>uc_stack</code>為該上下文中使用的棧;<code>uc_mcontext</code>儲存的上下文的特定機器表示,包括調用線程的特定寄存器等。
下面詳細介紹四個函數:
初始化ucp結構體,将目前的上下文儲存到ucp中
設定目前的上下文為ucp,setcontext的上下文ucp應該通過getcontext或者makecontext取得,如果調用成功則不傳回。如果上下文是通過調用getcontext()取得,程式會繼續執行這個調用。如果上下文是通過調用makecontext取得,程式會調用makecontext函數的第二個參數指向的函數,如果func函數傳回,則恢複makecontext第一個參數指向的上下文第一個參數指向的上下文context_t中指向的uc_link.如果uc_link為null,則線程退出。
makecontext修改通過getcontext取得的上下文ucp(這意味着調用makecontext前必須先調用getcontext)。然後給該上下文指定一個棧空間ucp->stack,設定後繼的上下文ucp->uc_link.
當上下文通過setcontext或者swapcontext激活後,執行func函數,argc為func的參數個數,後面是func的參數序列。當func執行傳回後,繼承的上下文被激活,如果繼承上下文為null時,線程退出。
儲存目前上下文到oucp結構體中,然後激活upc上下文。
如果執行成功,getcontext傳回0,setcontext和swapcontext不傳回;如果執行失敗,getcontext,setcontext,swapcontext傳回-1,并設定對于的errno.
簡單說來, <code>getcontext</code>擷取目前上下文,<code>setcontext</code>設定目前上下文,<code>swapcontext</code>切換上下文,<code>makecontext</code>建立一個新的上下文。
雖然我們稱協程是一個使用者态的輕量級線程,但實際上多個協程同屬一個線程。任意一個時刻,同一個線程不可能同時運作兩個協程。如果我們将協程的排程簡化為:主函數調用協程1,運作協程1直到協程1傳回主函數,主函數在調用協程2,運作協程2直到協程2傳回主函數。示意步驟如下:
這種設計的關鍵在于實作主函數到一個協程的切換,然後從協程傳回主函數。這樣無論是一個協程還是多個協程都能夠完成與主函數的切換,進而實作協程的排程。
實作使用者線程的過程是:
我們首先調用getcontext獲得目前上下文
修改目前上下文ucontext_t來指定新的上下文,如指定棧空間極其大小,設定使用者線程執行完後傳回的後繼上下文(即主函數的上下文)等
調用makecontext建立上下文,并指定使用者線程中要執行的函數
切換到使用者線程上下文去執行使用者線程(如果設定的後繼上下文為主函數,則使用者線程執行完後會自動傳回主函數)。
下面代碼<code>context_test</code>函數完成了上面的要求。
在context_test中,建立了一個使用者線程child,其運作的函數為func1.指定後繼上下文為main
func1傳回後激活後繼上下文,繼續執行主函數。
儲存上面代碼到example-switch.cpp.運作編譯指令:
執行程式結果如下
你也可以通過修改後繼上下文的設定,來觀察程式的行為。如修改代碼
為
再重新編譯執行,其執行結果為:
可以發現程式沒有列印"main",執行為func1後直接退出,而沒有傳回主函數。可見,如果要實作主函數到線程的切換并傳回,指定後繼上下文是非常重要的。
掌握了上一節從主函數到協程的切換的關鍵,我們就可以開始考慮實作自己的協程了。
定義一個協程的結構體如下:
ctx儲存協程的上下文,stack為協程的棧,棧大小預設為default_stack_szie=128kb.你可以根據自己的需求更改棧的大小。func為協程執行的使用者函數,arg為func的參數,state表示協程的運作狀态,包括free,runnable,runing,suspend,分别表示空閑,就緒,正在執行和挂起四種狀态。
在定義一個排程器的結構體
排程器包括主函數的上下文main,包含目前排程器擁有的所有協程的vector類型的threads,以及指向目前正在執行的協程的編号running_thread.如果目前沒有正在執行的協程時,running_thread=-1.
接下來,在定義幾個使用函數uthread_create,uthread_yield,uthread_resume函數已經輔助函數schedule_finished.就可以了。
建立一個協程,該協程的會加入到schedule的協程式列中,func為其執行的函數,arg為func的執行函數。傳回建立的線程在schedule中的編号。
挂起排程器schedule中目前正在執行的協程,切換到主函數。
恢複運作排程器schedule中編号為id的協程
判斷schedule中所有的協程是否都執行完畢,是傳回1,否則傳回0.注意:如果有協程處于挂起狀态時算作未全部執行完畢,傳回0.
代碼就不全貼出來了,我們來看看兩個關鍵的函數的具體實作。首先是uthread_resume函數:
如果指定的協程是首次運作,處于runnable狀态,則建立一個上下文,然後切換到該上下文。如果指定的協程已經運作過,處于suspend狀态,則直接切換到該上下文即可。代碼中需要注意runnbale狀态的地方不需要break.
uthread_yield挂起目前正在運作的協程。首先是将running_thread置為-1,将正在運作的協程的狀态置為suspend,最後切換到主函數上下文。
儲存下面代碼到example-uthread.cpp.
執行編譯指令并運作:
運作結果如下:
可以看到,程式協程func2,然後切換到主函數,在執行協程func3,再切換到主函數,又切換到func2,在切換到主函數,再切換到func3,最後切換到主函數結束。
總結一下,我們利用getcontext和makecontext建立上下文,設定後繼的上下文到主函數,設定每個協程的棧空間。在利用swapcontext在主函數和協程之間進行切換。
到此,使用ucontext做一個自己的協程庫就到此結束了。相信你也可以自己完成自己的協程庫了。