「游戏引擎Mojoc」(7)C使用goto label地址实现协程

来源:互联网 发布:网络新词语大全 编辑:程序博客网 时间:2024/06/09 15:19

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() {     static void* p = &&label;     goto *p;     printf("before label\n");     label:     printf("after label\n");     return 0;}
  • &&label是语法,&&就是获得label的地址,并不是取地址再取地址。
  • goto *p 就是跳转到p指针,所指向的地址。
  • 如果p指向了的是不正确的地址,程序会运行时崩溃。
  • label地址需要用void*指针存放。

思路

每一个抽象协程结构,除了执行函数,还会绑定状态,等待条件,参数等等。然后,会被注册到协程管理类。协程管理类,在update中每一帧去检测每个协程的状态,以决定执行的权限。而协程执行函数,相当于进行了分布计算。

协程等待类型

typedef enum{    /**     * Coroutine wait for frame count to waitValue     */     CoroutineWaitType_Frames,    /**     * Coroutine wait for second count to waitValue     */     CoroutineWaitType_Seconds,    /**     * Coroutine wait for other Coroutine to finish     */     CoroutineWaitType_Coroutines,    /**     * Coroutine just run forward     */     CoroutineWaitType_Null,}CoroutineWaitType;

协程让出执行权限后,可以等待帧,秒,其它协程三种类型。

协程的状态

typedef enum{    /**     * Coroutine enter queue ready to running     */     CoroutineState_Ready,    /**     * Coroutine has started to execute     */     CoroutineState_Running,    /**     * Coroutine already finished and waiting for reuse     */     CoroutineState_Finish,}CoroutineState;

等待执行,正在执行包括中断的也算在执行的,还有执行完成的。协程执行完成会进入缓存队列。

协程结构

typedef struct Coroutine Coroutine;typedef void   (*CoroutineRun)(Coroutine* coroutine);struct Coroutine{    /**     * Record coroutine run step     */    void*                 step;    /**     * Coroutine implement function     */    CoroutineRun          Run;    /**     * Coroutine current state     */    CoroutineState        state;    /**     * Coroutine wait value to execute     */    float                 waitValue;    /**     * Record wait progress     */    float                 curWaitValue;    /**     * Coroutine wait type     */    CoroutineWaitType     waitType;    /**     * Hold params for CoroutineRun to get     * when coroutine finish clear but the param create memory control yourself     */    ArrayList(void*)      params[1];    /**     * Hold Coroutines wait for this Coroutine to finish     */    ArrayList(Coroutine*) waits [1];};
  • [step] 用来保存CoroutineRun执行到哪一行了。下次继续这一行执行。后面会介绍,使用宏定义LINE来捕获函数执行的函数,保存到step。

  • [Run] 就是一个C语言的函数,真正执行的协程函数。

  • [state] 用来标示协程处在什么状态。

  • [waitValue] 表示协程等待的数值,帧数还是时间。

  • [curWaitValue] 就是当前等待了多少数值,这个值抵达waitValue表示协程等待结束了。

  • [waitType] 表示等待的类型。是等待帧数,还是时间,还是其它协程完成。

  • [params] 是绑定的一个动态数组,存放需要在协程函数里使用的参数。

  • [waits] 也是一个动态数组,存放的是等待当前协程的其它协程。也就是说有多个协程在等待这个协程,当这个协程完成的时候会释放等待队列的其它协程。这里并没有使用一个指针保存等待的协程,而是选择了保存等待自己的协程数组。因为协程使用了缓存系统,一个协程结束,就要进入缓存队列,依赖它的协程需要立马得到通知。

协程绑定数据

#define ACoroutine_AddParam(coroutine, value) \    AArrayList_Add(coroutine->params, value)/** * Get param value */#define ACoroutine_GetParam(coroutine, index, type) \    AArrayList_Get(coroutine->params, index, type)/** * Get param valuePtr */#define ACoroutine_GetPtrParam(coroutine, index, type) \    AArrayList_GetPtr(coroutine->params, index, type)

但协程让出执行的时候,除了static和全局变量,其的它局部变量都会丢失,所以这里提供了一个数组来保存,需要记住的数据。

协程标识

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

只有处在Begin和End宏之间,才能使用协程的中断函数。Begin的功能是在协程得到执行权限之后,直接调转到上次执行的地方继续执行。

协程中断函数

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

API模拟了Unity中的协程设计。调用这几个宏的时候,会使用宏LINE构建一个goto的行号标签,并把这个标签保存在step变量之中。然后函数就return了,下次Begin的时候,会直接goto到step保存的地址。

goto的标签要放在return的后面,这样会先return,下次接着return后面继续执行。

如何使用

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

最后

Mojoc的完整实现在这里Coroutine.h,Coroutine.c,游戏里使用协程来loading加载资源AppInit.c。


「值得一试」

原创粉丝点击