C 语言实现协程,最困难的部分就是上下文信息的保存和还原。这样才能够做到,让协程在任意位置让出执行权限,稍后再恢复到中断位置继续执行。C 实现协程一般有几个方案。
- 使用第三方库来保存恢复上下文数据,比如ucontext
- 使用汇编来保存上下文信息
- 使用setjmp / longjmp 保存恢复上下文信息
- 使用switch case的特性来做上下文断点继续,上下文信息需要用static变量保存。比如Protothreads
- 使用线程来保存上下文信息
本文,使用了switch case的特性来保存中断位置,使用数据结构和static变量来保存上下文信息,使用宏来构建API调用。由于我使用过lua和unity c#协程进行了产品开发。所以,这套实现会贴近unity中C#的使用习惯。完成了一下功能:
- 在协程执行的任意位置暂停,让出执行权限
- 恢复协程继续上次中断的地方继续执行
- 通过static变量和数据结构保存协程数据
- 协程让出执行后,等待特定的帧数,时间,和其它协程完成
开始看代码:
typedef enum { /** * Coroutine wait for frame count to waitValue */ coroutine_wait_frame, /** * Coroutine wait for second count to waitValue */ coroutine_wait_second, /** * Coroutine wait for other Coroutine to finish */ coroutine_wait_coroutine, /** * Coroutine just run forward */ coroutine_wait_none, } CoroutineWaitType;先定义协程让出执行后,等待的类型。可以看到这里定义了几种类型,可以等待帧数,时间,其它协程。
typedef enum { /** * Coroutine enter queue ready to running */ coroutine_state_ready, /** * Coroutine has started to execute */ coroutine_state_running, /** * Coroutine already finished and waiting for reuse */ coroutine_state_finish, } CoroutineState;
这里定义协程的状态。等待执行,正在执行包括中断的也算在执行的,还有执行完成的。我们后面会介绍,有一个协程管理器。所有的协程进入管理器,被轮询检测。完成后的协程会被缓存起来,下次请求协程的时候会先检查缓存的协程可否使用。
typedef struct Coroutine Coroutine; typedef void (*CoroutineRun)(Coroutine* coroutine); struct Coroutine { /** * Record coroutine run step */ int 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 types */ 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]; };
这里定以了一个协程的数据结构。CoroutineRun 就是一个C语言的函数,真正执行的协程函数。
- step 用来保存CoroutineRun执行到哪一行了。下次继续这一行执行。后面会介绍,使用宏定义 __LINE__来捕获函数执行的函数,保存到step。
- Run 就是执行的函数指针。
- state 用来标示协程处在什么状态。
- waitValue 表示协程等待的数值,帧数还是时间。
- curWaitValue 就是当前等待了多少数值,这个值抵达waitValue表示协程等待结束了。
- waitType 表示等待的类型。是等待帧数,还是时间,还是其它协程完成。
- params 是绑定的一个动态数组,存放需要在协程函数里使用的参数。ArrayList是自定义类型,可以替换为其它相同实现。后面的()仅仅是一个空参数的宏定义。
- waits 也是一个动态数组,存放的是等待当前协程的其它协程。也就是说有多个协程在等待这个协程,当这个协程完成的时候会释放等待队列的其它协程。这里并没有使用一个指针保存等待的协程,而是选择了保存等待自己的协程数组。因为协程使用了缓存系统,一个协程结束,就要进入缓存队列,依赖它的协程需要立马得到通知。
接下来,我们提供一组宏定义,用在 CoroutineRun 中,来完成协程的功能。
#define ACoroutineAddParam(coroutine, value) \ AArrayListAdd(coroutine->params, value) /** * return value */ #define ACoroutineGetParam(coroutine, index, type) \ AArrayListGet(coroutine->params, index, type) /** * return valuePtr */ #define ACoroutineGetPtrParam(coroutine, index, type) \ AArrayListGetPtr(coroutine->params, index, type)这是在协程对象上绑定和获取数据,为了在协程函数内使用外部数据。就是使用协程对象的params数组。
#define ACoroutineBegin() \ switch (coroutine->step) \ { \ case 0: \ coroutine->state = coroutine_state_running #define ACoroutineEnd() \ } \ coroutine->state = coroutine_state_finish \
这两个宏是协程主体功能的开始和结束。在这两段之内的代码,可以通过后面提供的宏进行中断。这里是建立了一个switch case代码段,协程的代码处在这个代码段中,就可以利用case任意跳转。每次跳转的位置由step标识。
#define ACoroutineYieldFrame(waitFrameCount) \ coroutine->waitValue = waitFrameCount; \ coroutine->curWaitValue = 0.0f; \ coroutine->waitType = coroutine_wait_frame; \ coroutine->step = __LINE__; \ return; \ case __LINE__: \ #define ACoroutineYieldSecond(waitSecond) \ coroutine->waitValue = waitSecond; \ coroutine->curWaitValue = 0.0f; \ coroutine->waitType = coroutine_wait_second; \ coroutine->step = __LINE__; \ return; \ case __LINE__: \ #define ACoroutineYieldCoroutine(waitCoroutine) \ coroutine->waitValue = 0.0f; \ coroutine->curWaitValue = 0.0f; \ coroutine->waitType = coroutine_wait_coroutine; \ AArrayListAdd((waitCoroutine)->waits, coroutine); \ coroutine->step = __LINE__; \ return; \ case __LINE__: \这里提供了,在begin和end之间中断的功能,等待帧数,等待时间,等待其它协程。原理是,使用这几个宏的时候,会用__LINE__赋值step,这样step就持有了当前行数变量。先return结束函数,在添加了case __LINE__,这样下次再次执行这个函数的时候,就会直接跳到上次return后的一个case上,继续执行。保存状态的变量需要使用static local变量保存,或是利用params传入。
#define ACoroutineYieldBreak() \ coroutine->state = coroutine_state_finish; \ return \中断协程就是设置状态直接跳出。由于在begin和end中可能嵌套有循环,所以不能break,要直接return。
那看看怎么使用:
static void CRun(Coroutine* coroutine) { ACoroutineBegin(); ALogD("### begin"); ACoroutineYieldSecond(5.0f); ALogD("### yield second 5"); ACoroutineYieldSecond(10.0f); ALogD("### yield second 10"); ACoroutineYieldFrame(100.0f); ALogD("### yield frame 100"); ACoroutineEnd(); } void main() { ACoroutine->StartCoroutine(CRun); }只要在begin和end之间,使用Yield就可以让出执行流程,然后在返回接着执行。再次强调,需要保存进度的变量,需要使用params保存或是static local变量。那么,让出执行流程,是如何恢复的呢。那是因为所有协程都在一个协程管理器。协程管理器每帧都会执行控制协程的流程。代码如下。
struct ACoroutine { /** * Bind CoroutineRun with Coroutine and enter queue ready to run */ Coroutine* (*StartCoroutine)(CoroutineRun Run); /** * Update on every frame */ void (*Update) (float deltaTime); }; extern struct ACoroutine ACoroutine[1];
协程管理器,需要一个CoroutineRun函数就可以启动,然后在 CoroutineRun 中使用协程的功能。协程管理器的完整实现如下。
static ArrayIntMap(Coroutine*) coroutineMap [1] = AArrayIntMapInit(Coroutine*, 20); static ArrayList (Coroutine*) coroutineList[1] = AArrayListInit (Coroutine*, 20); static Coroutine* StartCoroutine(CoroutineRun Run) { Coroutine* coroutine = AArrayListPop(coroutineList, Coroutine*); if (coroutine == NULL) { coroutine = (Coroutine*) malloc(sizeof(Coroutine)); AArrayList->Init(sizeof(void*), coroutine->params); coroutine->params->increase = 4; AArrayList->Init(sizeof(Coroutine*), coroutine->waits); coroutine->waits->increase = 4; } else { AArrayList->Clear(coroutine->params); AArrayList->Clear(coroutine->waits); } coroutine->Run = Run; coroutine->step = 0; coroutine->waitValue = 0.0f; coroutine->curWaitValue = 0.0f; coroutine->waitType = coroutine_wait_none; coroutine->state = coroutine_state_ready; AArrayIntMapPut(coroutineMap, coroutine, coroutine); return coroutine; } static void Update(float deltaTime) { for (int i = coroutineMap->arrayList->size - 1; i > -1; i--) { Coroutine* coroutine = AArrayIntMapGetAt(coroutineMap, i, Coroutine*); if (coroutine->waitType == coroutine_wait_coroutine) { continue; } else if (coroutine->curWaitValue >= coroutine->waitValue) { coroutine->Run(coroutine); if (coroutine->state == coroutine_state_finish) { AArrayIntMap->RemoveAt(coroutineMap, i); // add to cache AArrayListAdd(coroutineList, coroutine); // set waiting coroutines execute forward for (int j = 0; j < coroutine->waits->size; j++) { Coroutine* wait = AArrayListGet(coroutine->waits, j, Coroutine*); ALogA ( wait->state != coroutine_state_finish, "Coroutine [%p] can not finish before wait coroutine [%p] finish", wait, coroutine ); wait->waitType = coroutine_wait_none; } continue; } } else { switch (coroutine->waitType) { case coroutine_wait_frame: coroutine->curWaitValue += 1.0f; break; case coroutine_wait_second: coroutine->curWaitValue += deltaTime; break; } } } } struct ACoroutine ACoroutine[1] = { StartCoroutine, Update, };代码量很少,Update函数需要每帧都调用。 ArrayIntMap 和 ArrayList 就是自定义的字典映射和动态数组。我在开发游戏中使用过lua和unity的C#中的协程。这套实现也是模拟了unity里面协程的接口。最后,说一下个人理解的协程的好处。
协程,能够把一个计算或是操作,分解成若干步,并且可以再任何一步停下来,并在需要的时候继续执行剩下的步骤。
这样的模型,给予了更细粒度的控制一个操作或是功能。
比如,一个非常耗时间的操作,被分步执行可以更好的控制程序响应。
比如,一个操作需要依赖各种条件,可以更好的处理条件不满足时候的情况。
也能够更好的把操作或是计算过程中的状态变化,与其它的状态变化交互。而然,程序运行的过程就是抽象数据和结构不断变化的过程,协程能够优雅自然的进行这个变化过程的需求。
这样的模型,给予了更细粒度的控制一个操作或是功能。
比如,一个非常耗时间的操作,被分步执行可以更好的控制程序响应。
比如,一个操作需要依赖各种条件,可以更好的处理条件不满足时候的情况。
也能够更好的把操作或是计算过程中的状态变化,与其它的状态变化交互。而然,程序运行的过程就是抽象数据和结构不断变化的过程,协程能够优雅自然的进行这个变化过程的需求。