C 语言实现协程

时间:2022-04-13 01:10:16

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里面协程的接口。最后,说一下个人理解的协程的好处。


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

这样的模型,给予了更细粒度的控制一个操作或是功能。
比如,一个非常耗时间的操作,被分步执行可以更好的控制程序响应。
比如,一个操作需要依赖各种条件,可以更好的处理条件不满足时候的情况。

也能够更好的把操作或是计算过程中的状态变化,与其它的状态变化交互。而然,程序运行的过程就是抽象数据和结构不断变化的过程,协程能够优雅自然的进行这个变化过程的需求。