「遊戲引擎Mojoc」(7)C使用goto label地址實現協程

C 語言實現協程,最困難的部分就是上下文信息的保存和還原。這樣才能夠做到,讓協程在任意位置讓出執行許可權,稍後再恢復到中斷位置繼續執行。C 實現協程一般有幾個方案。

  • 使用第三方庫來保存恢復上下文數據,比如ucontext。
  • 使用彙編來保存上下文信息。
  • 使用setjmp / longjmp 保存恢復上下文信息。
  • 使用switch case的特性來做上下文斷點繼續,上下文信息需要用static變數保存。比如Protothreads。
  • 使用線程來保存上下文信息。

Mojoc使用了類似switch case的解決方案,利用數據結構和static變數來保存上下文信息,使用宏來構建API調用。在實現的過程中,我發現C99中goto label地址的方法,可以替換掉switch case的結構,從而解決了switch case嵌套的問題。 另外,在API的設計上,借鑒了Unity的協程設計。

這個C的協程,完成了一下幾個功能:

  • 在協程執行的任意位置暫停,讓出執行許可權。
  • 恢復協程繼續上次中斷的地方繼續執行。
  • 通過static變數和數據結構保存協程數據。
  • 協程讓出執行後,等待特定的幀數,時間,和其它協程完成。

goto label地址

C99中,goto語句可以跳轉到一個變數里,變數保存的是label的地址。

int main() n{n static void* p = &&label;n goto *p;nn printf("before labeln");n label:n printf("after labeln");nn return 0;n}n

  • &&label是語法,&&就是獲得label的地址,並不是取地址再取地址。
  • goto *p 就是跳轉到p指針,所指向的地址。
  • 如果p指向了的是不正確的地址,程序會運行時崩潰。
  • label地址需要用void*指針存放。

思路

每一個抽象協程結構,除了執行函數,還會綁定狀態,等待條件,參數等等。然後,會被註冊到協程管理類。協程管理類,在update中每一幀去檢測每個協程的狀態,以決定執行的許可權。而協程執行函數,相當於進行了分步計算。

協程等待類型

typedef enumn{n /**n * Coroutine wait for frame count to waitValuen */n CoroutineWaitType_Frames,nn /**n * Coroutine wait for second count to waitValuen */n CoroutineWaitType_Seconds,nn /**n * Coroutine wait for other Coroutine to finishn */n CoroutineWaitType_Coroutines,nn /**n * Coroutine just run forwardn */n CoroutineWaitType_Null,n}nCoroutineWaitType;n

協程讓出執行許可權後,可以等待幀,秒,其它協程三種類型。

協程的狀態

typedef enumn{n /**n * Coroutine enter queue ready to runningn */n CoroutineState_Ready,nn /**n * Coroutine has started to executen */n CoroutineState_Running,nn /**n * Coroutine already finished and waiting for reusen */n CoroutineState_Finish,n}nCoroutineState;n

等待執行,正在執行包括中斷的也算在執行的,還有執行完成的。協程執行完成會進入緩存隊列。

協程結構

typedef struct Coroutine Coroutine;ntypedef void (*CoroutineRun)(Coroutine* coroutine);nnnstruct Coroutinen{n /**n * Record coroutine run stepn */n void* step;nn /**n * Coroutine implement functionn */n CoroutineRun Run;nn /**n * Coroutine current staten */n CoroutineState state;nn /**n * Coroutine wait value to executen */n float waitValue;nn /**n * Record wait progressn */n float curWaitValue;nn /**n * Coroutine wait typen */n CoroutineWaitType waitType;nn /**n * Hold params for CoroutineRun to getn * when coroutine finish clear but the param create memory control yourselfn */n ArrayList(void*) params[1];nn /**n * Hold Coroutines wait for this Coroutine to finishn */n ArrayList(Coroutine*) waits [1];n};n

  • [step] 用來保存CoroutineRun執行到哪一行了。下次繼續這一行執行。後面會介紹,使用宏定義LINE來捕獲函數執行的函數,保存到step。
  • [Run] 就是一個C語言的函數,真正執行的協程函數。
  • [state] 用來標示協程處在什麼狀態。
  • [waitValue] 表示協程等待的數值,幀數還是時間。
  • [curWaitValue] 就是當前等待了多少數值,這個值抵達waitValue表示協程等待結束了。
  • [waitType] 表示等待的類型。是等待幀數,還是時間,還是其它協程完成。
  • [params] 是綁定的一個動態數組,存放需要在協程函數里使用的參數。
  • [waits] 也是一個動態數組,存放的是等待當前協程的其它協程。也就是說有多個協程在等待這個協程,當這個協程完成的時候會釋放等待隊列的其它協程。這裡並沒有使用一個指針保存等待的協程,而是選擇了保存等待自己的協程數組。因為協程使用了緩存系統,一個協程結束,就要進入緩存隊列,依賴它的協程需要立馬得到通知。

協程綁定數據

#define ACoroutine_AddParam(coroutine, value) n AArrayList_Add(coroutine->params, value)nnn/**n * Get param valuen */n#define ACoroutine_GetParam(coroutine, index, type) n AArrayList_Get(coroutine->params, index, type)nnn/**n * Get param valuePtrn */n#define ACoroutine_GetPtrParam(coroutine, index, type) n AArrayList_GetPtr(coroutine->params, index, type)n

但協程讓出執行的時候,除了static和全局變數,其的它局部變數都會丟失,所以這裡提供了一個數組來保存,需要記住的數據。

協程標識

#define ACoroutine_Begin() n if (coroutine->step != NULL) n { n goto *coroutine->step; n } n coroutine->state = CoroutineState_Running nnn#define ACoroutine_End() n coroutine->state = CoroutineState_Finishn

只有處在Begin和End宏之間,才能使用協程的中斷函數。Begin的功能是在協程得到執行許可權之後,直接調轉到上次執行的地方繼續執行。

協程中斷函數

/**n * Construct goto label with line numbern */n#define ACoroutine_StepName(line) Step##linen#define ACoroutine_Step(line) ACoroutine_StepName(line)nn/**n * Called between ACoroutine_Begin and ACoroutine_Endn *n * waitFrameCount: CoroutineRun wait frames and running againn */n#define ACoroutine_YieldFrames(waitFrames) n coroutine->waitValue = waitFrames; n coroutine->curWaitValue = 0.0f; n coroutine->waitType = CoroutineWaitType_Frames; n coroutine->step = &&ACoroutine_Step(__LINE__); n return; n ACoroutine_Step(__LINE__):nnn/**n * Called between ACoroutine_Begin and ACoroutine_Endn *n * waitSecond: CoroutineRun wait seconds and running againn */n#define ACoroutine_YieldSeconds(waitSeconds) n coroutine->waitValue = waitSeconds; n coroutine->curWaitValue = 0.0f; n coroutine->waitType = CoroutineWaitType_Seconds; n coroutine->step = &&ACoroutine_Step(__LINE__); n return; n ACoroutine_Step(__LINE__):nnn/**n * Called between ACoroutine_Begin and ACoroutine_Endn *n * waitCoroutine: CoroutineRun wait other Coroutine finished and running againn */n#define ACoroutine_YieldCoroutine(waitCoroutine) n coroutine->waitValue = 0.0f; n coroutine->curWaitValue = 0.0f; n coroutine->waitType = CoroutineWaitType_Coroutines; n AArrayList_Add((waitCoroutine)->waits, coroutine); n coroutine->step = &&ACoroutine_Step(__LINE__); n return; n ACoroutine_Step(__LINE__):nnn/**n * Called between ACoroutine_Begin and ACoroutine_Endn * sotp coroutine runningn */n#define ACoroutine_YieldBreak() n coroutine->state = CoroutineState_Finish; n returnn

API模擬了Unity中的協程設計。調用這幾個宏的時候,會使用宏LINE構建一個goto的行號標籤,並把這個標籤保存在step變數之中。然後函數就return了,下次Begin的時候,會直接goto到step保存的地址。

goto的標籤要放在return的後面,這樣會先return,下次接著return後面繼續執行。

如何使用

static void LoadingRun(Coroutine* coroutine)n{n static int progress = 0;n//--------------------------------------------------------------------------------------------------n ACoroutineBegin();n for (; progress < progressSize; progress++)n {n ACoroutineYieldFrame(0);n }n ACoroutineYieldSecond(1.0f);n ACoroutineEnd();n}nstatic void OnReady()n{n ACoroutine->StartCoroutine(LoadingRun);n}n

最後

Mojoc的完整實現在這裡Coroutine.h,Coroutine.c,遊戲里使用協程來loading載入資源AppInit.c。

推薦閱讀:

C 語言用 gcc 和 vs2013 編譯有什麼區別?
C中有沒有將一個函數轉變為另一個函數的函數(例如求導運算)?
為什麼 C99 標準都推出很長時間了,真正能夠完全支持 C99 的編譯器卻比較少?
如何用C/C++動手編程一款windows平台下的屬於自己的音樂播放器軟體?
c語言printf("xyz-123"+2)為什麼結果是z-123?

TAG:C编程语言 | 协程 | 手机游戏引擎 |