首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> C++ 入門

從零手寫一個輕量級 C++ 協程庫(基於 ucontext)

     編輯:C++ 入門

動機與設計目標

C++ 標準庫直到 C++20 才引入原生協程,且 Boost.Coroutine 接口較繁瑣。參考 Lua 協程的極簡風格,僅需四個核心接口即可覆蓋大多數場景:

  • create_coroutine(func, arg) — 創建協程,返回 ID
  • resume_coroutine(id, arg) — 恢復/啟動協程,傳入參數,返回 yield 值
  • yield(arg) — 掛起當前協程,傳回值給調用者
  • is_alive(id) — 判斷協程是否仍可恢復

核心原理:ucontext 上下文切換

POSIX 提供 ucontext.h 系列函數,可在用戶態保存/恢復 CPU 寄存器、棧指針、指令指針:

  • getcontext(&ctx) — 獲取當前上下文
  • makecontext(&ctx, func, argc, ...) — 修改上下文指向新函數入口
  • swapcontext(&ouctx, &inctx) — 原子切換:保存 ouctx,恢復 inctx

一個最小演示:

#include 
#include 
using namespace std;

static char stack[2048];
static ucontext_t ctx, ctx_main;

void func() {
    cout << "enter func" << endl;
    swapcontext(&ctx, &ctx_main);   // yield
    cout << "func resume from yield" << endl;
}

int main() {
    getcontext(&ctx);
    ctx.uc_stack.ss_sp = stack;
    ctx.uc_stack.ss_size = sizeof(stack);
    ctx.uc_link = &ctx_main;        // func 結束後返回 main
    makecontext(&ctx, func, 0);

    cout << "main: before coroutine" << endl;
    swapcontext(&ctx_main, &ctx);   // resume func
    cout << "main: back from first yield" << endl;
    swapcontext(&ctx_main, &ctx);   // resume again
    cout << "main: back again" << endl;
    return 0;
}

完整調度器實現要點

協程控制塊

struct Coroutine {
    ucontext_t ctx;       // 上下文
    char* stack;          // 獨立棧空間
    void (*func)(void*);  // 入口函數
    void* arg;            // 用戶參數
    uintptr_t yield_val;  // 雙向傳值緩衝
    enum { READY, RUNNING, SUSPENDED, FINISHED } status;
};

調度器核心邏輯

  • Resume:根據狀態分支
    • READY → getcontext、配置棧、makecontext 指向統一入口 SchedulerImpl::Schedule、swapcontext(main, &cor->ctx)
    • SUSPENDED → 直接 swapcontext(main, &cor->ctx)
  • Yield:保存傳值、標記 SUSPENDED、swapcontext(&cor->ctx, &main) 切回主上下文,返回主調用者傳入的值。
  • Schedule(統一入口):調用 cor->func(cor->arg),執行完畢標記 FINISHED、running_ = -1。

關鍵代碼(節選)

uintptr_t SchedulerImpl::ResumeCoroutine(int id, uintptr_t y) {
    Coroutine* cor = id2routine_[id];
    if (!cor || cor->status == RUNNING) return 0;
    cor->yield_val = y;

    switch (cor->status) {
        case READY: {
            getcontext(&cor->ctx);
            cor->ctx.uc_stack.ss_sp = cor->stack;
            cor->ctx.uc_stack.ss_size = stack_size_;
            cor->ctx.uc_link = &main_ctx_;
            running_ = id;
            cor->status = RUNNING;
            makecontext(&cor->ctx, (void(*)())Schedule, 1, this);
            swapcontext(&main_ctx_, &cor->ctx);
            break;
        }
        case SUSPENDED: {
            running_ = id;
            cor->status = RUNNING;
            swapcontext(&main_ctx_, &cor->ctx);
            break;
        }
    }
    uintptr_t ret = cor->yield_val;
    if (running_ == -1 && cor->status == FINISHED) DestroyCoroutine(id);
    return ret;
}

uintptr_t SchedulerImpl::Yield(uintptr_t y) {
    if (running_ < 0) return 0;
    int cur = running_;
    running_ = -1;
    Coroutine* cor = id2routine_[cur];
    cor->yield_val = y;
    cor->status = SUSPENDED;
    swapcontext(&cor->ctx, &main_ctx_);
    return cor->yield_val;  // 返回 resume 時傳入的值
}

使用示例

#include "coroutine.h"
#include 
using namespace std;

CoroutineScheduler* sched = nullptr;

void func1(void* arg) {
    uintptr_t ret;
    cout << "func1 start, arg=" << arg << ", yield 1" << endl;
    ret = sched->Yield((uintptr_t)"func1 yield 1");
    cout << "func1 resume, got: " << (const char*)ret << endl;
    ret = sched->Yield((uintptr_t)"func1 yield 2");
    cout << "func1 resume, got: " << (const char*)ret << ", exit" << endl;
}

void func2(void* arg) {
    cout << "func2 start, arg=" << arg << ", yield 1" << endl;
    const char* y = (const char*)sched->Yield((uintptr_t)"func2 yield 1");
    cout << "func2 resume, got: " << y << ", exit" << endl;
}

int main() {
    sched = new CoroutineScheduler();
    int f1 = sched->CreateCoroutine(func1, (void*)111);
    int f2 = sched->CreateCoroutine(func2, (void*)222);

    bool stop = false;
    while (!stop) {
        stop = true;
        if (sched->IsCoroutineAlive(f1)) {
            stop = false;
            const char* y = (const char*)sched->ResumeCoroutine(f1, (uintptr_t)"resume func1");
            cout << "main: func1 yielded -> " << y << endl;
        }
        if (sched->IsCoroutineAlive(f2)) {
            stop = false;
            const char* y = (const char*)sched->ResumeCoroutine(f2, (uintptr_t)"resume func2");
            cout << "main: func2 yielded -> " << y << endl;
        }
    }
    delete sched;
    return 0;
}

輸出結果

func1 start, arg=111, yield 1
main: func1 yielded -> func1 yield 1
func2 start, arg=222, yield 1
main: func2 yielded -> func2 yield 1
func1 resume, got: resume func1
main: func1 yielded -> func1 yield 2
func2 resume, got: resume func2
main: func2 yielded -> func2 yield 1
func1 resume, got: resume func1
main: func1 yielded -> func1 yield 2

優缺點與遷移建議

優點缺點
代碼極簡(~200 行)、無第三方依賴依賴 POSIX ucontext,Windows 原生不支持
語法接近 Lua,雙向傳值直觀棧大小固定,深度遞歸可能溢出
適合教學、嵌入式、輕量任務調度無法利用多核(單線程協作式)

現代項目建議遷移至 C++20 原生協程(co_await/co_yield)或成熟庫(libco、boost::coroutine2、asio::awaitable),以獲得標準化、跨平台、編譯器優化支持。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved