您的位置:首页 > 编程语言 > Go语言

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

2017-10-23 10:35 567 查看
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.hCoroutine.c,游戏里使用协程来loading加载资源AppInit.c

「值得一试」
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: