為什麼是 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_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) 之用。兩者的定義如下:
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),可以直接下載檔案進行參考。