為什麼是 Coroutine?(二)- 使用 C語言實作 Coroutine

前言

在前一篇文章 為什麼是 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 中定義了以下幾個成員:

1ucontext_t *uc_link     pointer to the context that will be resumed
2                        when this context returns
3sigset_t    uc_sigmask  the set of signals that are blocked when this
4                        context is active
5stack_t     uc_stack    the stack used by this context
6mcontext_t  uc_mcontext a machine-specific representation of the saved
7		                          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) 之用。兩者的定義如下:

1#include <ucontext.h>
2
3int getcontext(ucontext_t *ucp);
4int setcontext(const ucontext_t *ucp);

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

makecontext / swapcontext

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

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

作法

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

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

1#define _XOPEN_SOURCE
2
3#include <ucontext.h>
4#include <stdlib.h>
5#include <stdio.h>
6
7#define handle_error(msg) \
8do {perror(msg); exit(EXIT_FAILURE); } while(0)

Step 1: 定義 Consumer

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

1typedef struct Coroutine {
2    ucontext_t *ctx;
3    char func_stack[16384];
4    void* param;
5    bool is_finished;
6    struct Coroutine *next;
7} coroutine_t;

Step 2: 定義 Producer

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

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

Step 3: 定義 Producer 的任務

 1static co_registry_t *registry;
 2static ucontext_t main_ctx;
 3
 4static void
 5main_ctx_function ()
 6{
 7printf("main context: get control\n");
 8    if (!regstry->cur_coroutine) return;
 9
10    // scheduling
11    while (1) {
12        // find the next coroutine
13        coroutine_t* cur = registry->cur_coroutine;
14        coroutine_t* ptr = cur->next;
15        
16        // 檢查下一個子任務是否已經完成,若是就往指向下一個任務
17        while (ptr->is_finished) {
18        ptr = ptr->next;
19        if (ptr == cur) break;
20        }
21    
22        // 所有任務皆完成
23        if ((cur->is_finished) && (ptr == cur)) break;
24        
25        // 移至下一個任務
26        registry->cur_coroutine = ptr;
27        print("main coroutine: switch\n");
28        if (swapcontext(registry->ctx, registry->cur_coroutine->ctx) == -1)
29            handle_error("swapcontext");
30    }
31}

Step 4: 創建 Producer 的 context

 1co_registry_t *
 2create_producer ()
 3{
 4    co_registry_t *p;
 5    p = malloc(sizeof(co_registry_t));
 6    p->ctx = malloc(sizeof(ucontext_t));
 7
 8    if (getcontext(p->ctx) == -1)
 9        handle_error("getcontext");
10
11    p->ctx->uc_stack.ss_sp = p->func_stack;
12    p->ctx->uc_stack.ss_size = sizeof(p->func_stack);
13
14    makecontext->ctx, main_ctx_function, 0);
15
16    return p;
17}

Step 5: 創建 Consumer 的 context

 1/* create a new coroutine for a new task */
 2coroutine_t *
 3create_coroutine(coroutine_body_t func, void *param)
 4{
 5    coroutine_t *p;
 6    p = malloc(sizeof(coroutine_t));
 7    p->ctx = malloc(sizeof(ucontext_t));
 8
 9    // set up the created coroutine
10    if (getcontext(p->ctx) == -1)
11        handle_error("getcontext");
12
13    p->ctx->uc_stack.ss_sp = p->func_stack;
14    p->ctx->uc_stack.ss_size = sizeof(p->func_stack);
15    p->param = param;
16    p->is_finished = false;
17    p->next = NULL;
18
19    makecontext(p->ctx, func, 0);
20    return p;
21}

Step 6: 實作 yield

1/* return the control to the administer, and
2* determine which would be the next context 
3* by the administer.
4*/
5void
6yield()
7{
8    swapcontext(registry->cur_coroutine->ctx, registry->ctx);
9}

Step 7: 註冊 Coroutine

 1void 
 2register_coroutine(coroutine_t *coroutine)
 3{
 4    if (!registry->coroutines)
 5    {
 6        registry->coroutines = coroutine;
 7        registry->cur_coroutine = coroutine;
 8    }
 9    else
10    {
11        coroutine_t *ptr = registry->coroutines;
12        while (ptr->next != NULL) ptr = ptr->next;
13        ptr->next = coroutine;
14    }
15}

Step 8: 主程式

 1int main () {
 2    registry = create_co_registry();
 3
 4    print("creating coroutines...\n");
 5        // func1, func2 can be any user defined static function
 6    coroutine_t *co_1 = create_coroutine(func1, (void*) 0);
 7    coroutine_t *co_2 = create_coroutine(func2, (void*) 0);
 8
 9    printf("registering coroutines...\n");
10    register_coroutine(co_1);
11    register_coroutine(co_2);
12    co_2->next = co_1;
13
14    printf("starting coroutines...\n");
15    if (swapcontext(&main_ctx, registry->ctx) == -1)
16        handle_error("swapcontext");
17
18    free(registry);
19    free(co_1);
20    free(co_2);
21}

小結

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

參考資料