C 语言实现协程,最困难的部分就是上下文信息的保存和还原。这样才能够做到,让协程在任意位置让出执行权限,稍后再恢复到中断位置继续执行。C 实现协程一般有几个方案。
Mojoc使用了类似switch case的解决方案,利用数据结构和static变量来保存上下文信息,使用宏来构建API调用。在实现的过程中,我发现C99中goto label地址的方法,可以替换掉switch case的结构,从而解决了switch case嵌套的问题。 另外,在API的设计上,借鉴了Unity的协程设计。
这个C的协程,完成了一下几个功能:
C99中,goto语句可以跳转到一个变量里,变量保存的是label的地址。
int main()
{
static void* p = &&label;
goto *p;
printf("before label\n");
label:
printf("after label\n");
return 0;
}
每一个抽象协程结构,除了执行函数,还会绑定状态,等待条件,参数等等。然后,会被注册到协程管理类。协程管理类,在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。
「值得一试」