前言
在前一篇文章 為什麼是 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_link、uc_sigmask、uc_stack、uc_mcontext。
uc_link:ucontext_t 的指標,指向當前 context 交出執行權後,要返回的 context。
uc_sigmask:sigset_t 定義於 <signal.h> 之中,當前 context 執行時,uc_sigmask 底下的訊號會被阻斷
uc_stack:存放 function stack, program count 的地方
uc_mcontext:各機器定義的 context 表達方式
getcontext / setcontext
getcontext 與 setcontext 定義於 <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),可以直接下載檔案進行參考。