协程设计原理

协程设计原理

协程(Coroutine)是一种程序组件,允许在单个线程内实现多个执行流的协作式多任务。它在不涉及内核态切换的情况下,通过保存和恢复执行上下文(寄存器、栈)来实现任务的挂起与恢复,从而以同步的编程方式获得异步的高性能。

一、为什么需要协程

在传统的网络编程中,我们面临两种选择:同步阻塞模型和异步非阻塞模型。

  • 同步阻塞模型:代码直观,逐行执行,但每个I/O操作都会阻塞线程,导致线程资源浪费,并发能力受限。例如,串行发送多个DNS请求,总耗时等于各请求耗时之和。
  • 异步非阻塞模型:通过回调函数处理I/O就绪事件,可以在单个线程内并发处理大量I/O,性能极高。但回调嵌套容易导致“回调地狱”,代码难以维护和调试。

协程的目标:用同步的编程方式,实现异步的性能。即在协程内部,代码看起来是顺序执行的,但底层在I/O等待时会自动挂起并切换到其他就绪协程,从而充分利用CPU和I/O资源。

二、协程的基本概念

1. 协程的本质

协程是独立于语言的通用框架,不是某个语言特有的特性。它通过编译器或库的支持,实现函数的挂起与恢复。

2. 原语操作

协程的核心原语是 yield 和 resume:

  • yield:主动让出CPU,保存当前执行上下文(寄存器、栈),并切换回调度器。
  • resume:从调度器切换到指定协程,恢复其上下文并继续执行。

3. 有栈协程与无栈协程

  • 有栈协程:每个协程拥有独立的栈空间,可以保存局部变量和函数调用链。实现复杂,但支持任意嵌套调用。
  • 无栈协程:协程共享调用者栈,通过状态机实现挂起。轻量,但限制较多(如不能跨函数调用挂起)。

多数高性能协程库(如libco)采用有栈设计。

三、协程的实现方式

协程切换的本质是保存和恢复CPU寄存器(包括栈指针、指令指针等)。下面介绍三种常见的实现技术。

1. setjmp/longjmp

setjmp 保存当前函数环境(寄存器、栈指针等)到 jmp_buf 结构,longjmp 可以跳回 setjmp 的位置并恢复环境。

#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void func() {
    printf("Enter func\n");
    longjmp(env, 2); // 跳回setjmp处,并返回2
}

int main() {
    int val = setjmp(env);
    if (val == 0) {
        printf("First call, val=%d\n", val);
        func();
    } else {
        printf("Return from longjmp, val=%d\n", val);
    }
    return 0;
}

局限性:不管理栈空间,可能导致栈溢出;跨线程使用不安全。仅适合简单场景。

2. ucontext

ucontext 是 POSIX 提供的上下文操作接口,可以创建、切换和销毁用户级上下文。每个上下文包含完整的寄存器状态和独立的栈空间。

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

ucontext_t main_ctx, ctx1, ctx2;
char stack1[8192], stack2[8192];

void func1() {
    printf("In func1\n");
    swapcontext(&ctx1, &main_ctx); // 切回main
}

void func2() {
    printf("In func2\n");
    swapcontext(&ctx2, &main_ctx);
}

int main() {
    getcontext(&ctx1);
    ctx1.uc_stack.ss_sp = stack1;
    ctx1.uc_stack.ss_size = sizeof(stack1);
    ctx1.uc_link = &main_ctx; // 结束后返回main
    makecontext(&ctx1, func1, 0);

    getcontext(&ctx2);
    ctx2.uc_stack.ss_sp = stack2;
    ctx2.uc_stack.ss_size = sizeof(stack2);
    ctx2.uc_link = &main_ctx;
    makecontext(&ctx2, func2, 0);

    printf("Start switching\n");
    swapcontext(&main_ctx, &ctx1); // 切换到ctx1
    printf("Back from ctx1\n");
    swapcontext(&main_ctx, &ctx2);
    printf("Back from ctx2\n");
    return 0;
}

ucontext 提供了完整的上下文管理,是许多协程库的基础。但它是 Linux 特有的,且性能略逊于汇编实现。

四、协程调度器设计

协程调度器负责管理多个协程的生命周期和运行顺序。通常包含以下组件:

  • 就绪队列:存放可运行的协程,通常采用队列实现FCFS或优先级调度。
  • 等待队列:存放因I/O或超时而阻塞的协程,常使用红黑树(以超时时间或文件描述符为键)实现高效查找。
  • 事件驱动引擎:如epoll,用于监听I/O事件,当I/O就绪时将对应协程移回就绪队列。

调度器核心循环:

while (1) {
    // 处理超时:将超时的协程移回就绪队列
    process_timeout();

    // 执行所有就绪协程
    while (!ready_queue.empty()) {
        Coroutine* co = ready_queue.pop();
        resume(co);  // 切换到该协程
        if (co->state == FINISHED) {
            delete co;
        } else if (co->state == WAITING) {
            waiting_set.insert(co);
        }
    }

    // 等待I/O事件,设置超时时间为最近等待协程的超时时间
    int timeout = calc_timeout();
    int n = epoll_wait(epfd, events, maxevents, timeout);
    for (int i = 0; i < n; ++i) {
        int fd = events[i].data.fd;
        Coroutine* co = fd_to_co[fd];
        waiting_set.erase(co);
        ready_queue.push(co); // I/O就绪,重新调度
    }
}

协程定义示例

typedef struct Coroutine {
    int id;
    ucontext_t ctx;
    void *stack;
    int state; // READY, RUNNING, WAITING, FINISHED
    int fd;    // 等待的文件描述符
    // 其他字段
} Coroutine;

五、Hook机制:透明接入协程

为了让现有同步代码无需修改即可运行在协程之上,需要hook系统I/O函数(如read、write、recv、send、connect等)。通过动态链接库的符号替换,将标准库函数替换为自定义版本,在自定义版本中实现自动yield。

Hook实现原理

  1. 定义与系统调用同名的函数,如ssize_t read(int fd, void *buf, size_t count)。
  2. 在函数内部,首先判断fd是否已经设置为非阻塞。
  3. 调用poll或epoll检测fd是否就绪,若未就绪则执行yield,将当前协程挂起,并将fd加入监听集合。
  4. 当I/O就绪后,调度器恢复协程,此时再次调用真正的read(通过dlsym(RTLD_NEXT, "read")获取原始函数地址)即可立即返回数据。
ssize_t read(int fd, void *buf, size_t count) {
    // 检查是否在协程环境中
    if (!is_in_coroutine()) {
        return sys_read(fd, buf, count);
    }

    // 等待fd可读
    while (1) {
        struct pollfd pfd = {fd, POLLIN, 0};
        int ret = poll(&pfd, 1, 0);
        if (ret > 0) break; // 已就绪

        // 未就绪,将当前协程挂起,并注册fd监听
        co_yield_and_add_fd(fd, POLLIN);
    }

    // 调用真正的read
    return sys_read(fd, buf, count);
}

通过hook,像hiredis这样的客户端库无需修改即可在协程中高效运行。

相关资料:https://github.com/wangbojing/NtyCo.git

posted @ 2026-03-12 23:18  xggx  阅读(61)  评论(0)    收藏  举报