您的位置:首页 > 其它

C 语言实现协程

2016-11-24 18:41 127 查看
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函数需要每帧都调用。ArrayIntMapArrayList
就是自定义的字典映射和动态数组。我在开发游戏中使用过lua和unity的C#中的协程。这套实现也是模拟了unity里面协程的接口。最后,说一下个人理解的协程的好处。

协程,能够把一个计算或是操作,分解成若干步,并且可以再任何一步停下来,并在需要的时候继续执行剩下的步骤。

这样的模型,给予了更细粒度的控制一个操作或是功能。

比如,一个非常耗时间的操作,被分步执行可以更好的控制程序响应。

比如,一个操作需要依赖各种条件,可以更好的处理条件不满足时候的情况。

也能够更好的把操作或是计算过程中的状态变化,与其它的状态变化交互。而然,程序运行的过程就是抽象数据和结构不断变化的过程,协程能够优雅自然的进行这个变化过程的需求。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: