前言

在前一篇文章 為什麼是 Coroutine?(ㄧ)- Multithreading 不好嗎? 中,我們講解了 Coroutine 的相關原理。除此之外,我們也列舉出如果想要使用 C 語言實作 Coroutine 的話,我們需要的介面。在這篇文章中,我們會透過程式碼逐行的解釋如何完成 C 語言的 Coroutine。

source code:GanniPiece/SimpleCCoroutine: A simple coroutine example implemented using C (github.com)

背景知識

在開始之前,若您對 Coroutine 不太了解的話,可以先閱讀上一篇文章: 為什麼是 Coroutine?(ㄧ)- Multithreading 不好嗎?。 在前一篇文章中我們提到,我們可以透過 <ucontext.h> 中定義的以下四種行為對 ucontext_t [1] 進行操作。

makecontext [2] setcontext [3] getcontext [4] swapcontext [5]

若您對這些操作已有概念,可以直接跳至下個章節 作法 進行實作。

ucontext_t

現在我們先來看 ucontext_t 是什麼? ucontext_t 被定義在 <ucontext.h> 的標頭檔中,全名是 user context。在該標頭檔中透過了 typedef 定義了 mcontext_t 的資料型態。除此之外,其中亦定義了 ucontext_t 的 struct。在 ucontext_t 的 struct 中定義了以下幾個成員:

ucontext_t *uc_link     pointer to the context that will be resumed
                        when this context returns
sigset_t    uc_sigmask  the set of signals that are blocked when this
                        context is active
stack_t     uc_stack    the stack used by this context
mcontext_t  uc_mcontext a machine-specific representation of the saved
		                          context

這四個成員分別是 uc_linkuc_sigmaskuc_stackuc_mcontext

uc_linkucontext_t 的指標,指向當前 context 交出執行權後,要返回的 context。 uc_sigmasksigset_t 定義於 <signal.h> 之中,當前 context 執行時,uc_sigmask 底下的訊號會被阻斷 uc_stack:存放 function stack, program count 的地方 uc_mcontext:各機器定義的 context 表達方式

getcontext / setcontext

getcontextsetcontext 定義於 <ucontext.h> 之中,此兩者作為取得與設定使用者 context (user context) 之用。兩者的定義如下:

#include <ucontext.h>

int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);

getcontext() 會將 ucp 指向的 struct 初始化至當前使用者呼叫的執行序的 context 之中。至於 setcontext() 則恢復 ucp 指向的 context。

makecontext / swapcontext

#include <ucontext.h>
void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

makecontext() 指定了 ucp 指向的 func,當今天一個 context 透過 swapcontext() 或是 setcontext() 指向該 ucp 後,會將 argc 的參數傳入,並繼續執行該 func 的內容。 有了這四個方法的概念後,我們接下來再來看下一章的作法。

作法

在此區詳細作法,最好是能有文字附上圖片說明。 關於作法的說明請參考上一篇 為什麼是 Coroutine?(ㄧ)- Multithreading 不好嗎? 之 作法。

Step 0: 前置處理與巨集定義

#define _XOPEN_SOURCE

#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
do {perror(msg); exit(EXIT_FAILURE); } while(0)

Step 1: 定義 Consumer

Consumer 作為消費者,有自己的任務需要完成,在任務之中有可能會被中斷 (yield)。

typedef struct Coroutine {
    ucontext_t *ctx;
    char func_stack[16384];
    void* param;
    bool is_finished;
    struct Coroutine *next;
} coroutine_t;

Step 2: 定義 Producer

Producer 作為主要的 Coroutine,可以註冊消費者,亦需排程消費者。

typedef struct Registry {
    ucontext_t *ctx;
    char func_stack[16384];
    coroutine_t *cur_coroutine;		// 當前運行的 coroutine
    coroutine_t *coroutines;			// coroutine 列表
} co_registry_t;

Step 3: 定義 Producer 的任務

static co_registry_t *registry;
static ucontext_t main_ctx;

static void
main_ctx_function ()
{
printf("main context: get control\n");
    if (!regstry->cur_coroutine) return;

    // scheduling
    while (1) {
        // find the next coroutine
        coroutine_t* cur = registry->cur_coroutine;
        coroutine_t* ptr = cur->next;
        
        // 檢查下一個子任務是否已經完成,若是就往指向下一個任務
        while (ptr->is_finished) {
        ptr = ptr->next;
        if (ptr == cur) break;
        }
    
        // 所有任務皆完成
        if ((cur->is_finished) && (ptr == cur)) break;
        
        // 移至下一個任務
        registry->cur_coroutine = ptr;
        print("main coroutine: switch\n");
        if (swapcontext(registry->ctx, registry->cur_coroutine->ctx) == -1)
            handle_error("swapcontext");
    }
}

Step 4: 創建 Producer 的 context

co_registry_t *
create_producer ()
{
    co_registry_t *p;
    p = malloc(sizeof(co_registry_t));
    p->ctx = malloc(sizeof(ucontext_t));

    if (getcontext(p->ctx) == -1)
        handle_error("getcontext");

    p->ctx->uc_stack.ss_sp = p->func_stack;
    p->ctx->uc_stack.ss_size = sizeof(p->func_stack);

    makecontext->ctx, main_ctx_function, 0);

    return p;
}

Step 5: 創建 Consumer 的 context

/* create a new coroutine for a new task */
coroutine_t *
create_coroutine(coroutine_body_t func, void *param)
{
    coroutine_t *p;
    p = malloc(sizeof(coroutine_t));
    p->ctx = malloc(sizeof(ucontext_t));

    // set up the created coroutine
    if (getcontext(p->ctx) == -1)
        handle_error("getcontext");

    p->ctx->uc_stack.ss_sp = p->func_stack;
    p->ctx->uc_stack.ss_size = sizeof(p->func_stack);
    p->param = param;
    p->is_finished = false;
    p->next = NULL;

    makecontext(p->ctx, func, 0);
    return p;
}

Step 6: 實作 yield

/* return the control to the administer, and
* determine which would be the next context 
* by the administer.
*/
void
yield()
{
    swapcontext(registry->cur_coroutine->ctx, registry->ctx);
}

Step 7: 註冊 Coroutine

void 
register_coroutine(coroutine_t *coroutine)
{
    if (!registry->coroutines)
    {
        registry->coroutines = coroutine;
        registry->cur_coroutine = coroutine;
    }
    else
    {
        coroutine_t *ptr = registry->coroutines;
        while (ptr->next != NULL) ptr = ptr->next;
        ptr->next = coroutine;
    }
}

Step 8: 主程式

int main () {
    registry = create_co_registry();

    print("creating coroutines...\n");
        // func1, func2 can be any user defined static function
    coroutine_t *co_1 = create_coroutine(func1, (void*) 0);
    coroutine_t *co_2 = create_coroutine(func2, (void*) 0);

    printf("registering coroutines...\n");
    register_coroutine(co_1);
    register_coroutine(co_2);
    co_2->next = co_1;

    printf("starting coroutines...\n");
    if (swapcontext(&main_ctx, registry->ctx) == -1)
        handle_error("swapcontext");

    free(registry);
    free(co_1);
    free(co_2);
}

小結

在這篇文章中,我們接續上一篇 為什麼是 Coroutine?(ㄧ)- Multithreading 不好嗎?,以實際的程式碼來看如何用 C 語言來實作 Coroutine。我們亦將原始碼公布於 GanniPiece/SimpleCCoroutine: A simple coroutine example implemented using C (github.com),可以直接下載檔案進行參考。

參考資料