电子产业一站式赋能平台

PCB联盟网

搜索
查看: 56|回复: 0
收起左侧

100行代码实现无栈协程

[复制链接]

570

主题

570

帖子

7063

积分

高级会员

Rank: 5Rank: 5

积分
7063
发表于 前天 09:01 | 显示全部楼层 |阅读模式
先看测试代码:https://godbolt.org/z/zYT6bon36
无栈协程的基本原理如果要实现无栈协程,有个核心问题,如何让一个函数能够在执行过程中暂停,并在之后从暂停点继续执行?
传统函数一旦开始执行,就会一直运行到返回为止,是不能中途暂停后再恢复的。
状态机方法最直接的解决方案是使用状态机。通过保存当前执行状态,函数可以在下次调用时从上次离开的地方继续:
int function(int *state) {
    switch (*state) {
        case0:
            // 执行第一部分代码
            *state = 1;
            return0;  // 返回,下次从case 1开始
        case1:
            // 执行第二部分代码
            *state = 2;
            return0;  // 返回,下次从case 2开始
        case2:
            // 执行最后部分代码
            *state = 0;  // 重置状态
            return1;  // 完成
    }
}
使用这个函数的代码可能是这样的:
int main() {
    int state = 0;  // 初始状态
    while (function(&state) == 0) {
        // 函数尚未完成,可以做其他事情
        process_events();
        sleep_or_yield();
    }
    // 函数执行完成
    return 0;
}
这种方法的工作原理很清晰:
  • 使用一个状态变量跟踪执行进度
  • 每次调用时,从对应的状态继续执行
  • 执行一部分代码后,更新状态并返回
  • 函数返回值指示是否完成[/ol]虽然这种方法可行,但随着逻辑复杂度增加,状态数量也会迅速增长,状态管理肯定会变得非常复杂,代码变得难以维护。
    利用switch-case实现更自然的控制流我们能否让暂停和恢复的代码看起来更自然,更接近普通的顺序代码?
    这就是无栈协程的核心思想,我们其实可以通过巧妙使用C语言的switch-case语句,实现更优雅的解决方案。
    看下面代码(这段代码就是protothreads宏展开后的代码):
    static char task_function(state_t *state) {
        staticint counter;
        {
            char yield_flag = 1;
            switch (state->line) {
                case0:;  // 初始入口点
                    for (counter = 0; counter 32; ++counter) {
                        do {
                            do {
                                state->line = 81;  // 保存当前行号
                                case81:;         // 恢复点
                                    if (!(resource_available())) {
                                        return0;     // 资源不可用,返回等待
                                    }
                            } while (0);
                            consume_resource(); // 消费资源
                        } while (0);
                        process_data();  // 执行核心业务逻辑
                        release_resource();  // 释放资源
                    }
            };
            yield_flag = 0;
            state->line = 0;
            return3;  // 任务完成
        };
    }
    这段代码看起来很奇怪,但它实现了一个无栈协程!
    核心代码这里利用了C语言switch-case语句的一个不常用特性:case标签可以出现在复合语句内部。这样我们可以在代码的任何位置设置恢复点。
    核心机制解析代码的关键部分如下:
    state->line = 81;  // 保存当前行号
    case 81:;         // 恢复点
        if (!(resource_available())) {
            return 0;     // 资源不可用,返回等待
        }
    这段代码实现了阻塞等待的功能:
  • state->line = 81 保存当前执行位置的行号
  • 紧随其后的 case 81: 标签成为恢复点
  • 检查条件是否满足,不满足则返回
  • 当函数再次被调用时,switch (state->line) 会直接跳转到 case 81:,跳过之前已执行的代码[/ol]执行流程假设我们调用这个函数的流程如下:
  • 第一次调用
  • state->line 为0,执行从 case 0: 开始
  • 进入for循环,设置 state->line = 81
  • 检查资源是否可用,假设不可用
  • 返回0,表示需要等待[/ol]
  • 第二次调用
  • switch (state->line) 直接跳转到 case 81:
  • 再次检查资源是否可用,假设仍不可用
  • 再次返回0[/ol]
  • 第三次调用
  • 再次跳转到 case 81:
  • 检查资源,假设现在可用了
  • 执行 consume_resource()
  • 然后执行 process_data() 和 release_resource()
  • 继续循环,设置下一个恢复点...[/ol]
  • 最终调用
  • 当for循环完成后,执行 state->line = 0
  • 返回3,表示任务完成[/ol][/ol]为什么使用do-while(0)?宏中使用 do { ... } while (0) 很常见了。这种模式有几个作用:
  • 方便创建局部作用域,避免变量命名冲突
  • 确保宏展开后的代码结构正确
  • 可以在复合语句中使用多个语句[/ol]实现细节首先,需要一个结构体来保存执行状态:
    typedef unsigned short line_t;  // 行号类型
    struct task_state {
    line_t line;  // 当前执行行号
    };
    接下来,我们定义任务的几种状态:
    #define TASK_WAITING 0   // 任务等待中
    #define TASK_YIELDED 1   // 任务主动让出
    #define TASK_EXITED  2   // 任务退出
    #define TASK_ENDED   3   // 任务正常结束
    为了简化无栈协程的编写,可以定义一系列宏:
    // 初始化任务状态
    #define TASK_INIT(s)            (s)->line = 0
    // 开始任务函数体
    #define TASK_BEGIN(s)           { char yield_flag = 1; switch((s)->line) { case 0:
    // 结束任务函数体
    #define TASK_END(s)             } yield_flag = 0; (s)->line = 0; return TASK_ENDED; }
    // 等待条件满足
    #define TASK_WAIT_UNTIL(s, condition)  \
      do { \
        (s)->line = __LINE__;
        case __LINE__:; \
        if(!(condition)) { \
          return TASK_WAITING; \
        } \
      } while(0)
    // 主动让出执行权
    #define TASK_YIELD(s) \
      do { \
        yield_flag = 0; \
        (s)->line = __LINE__;
        case __LINE__:; \
        if(yield_flag == 0) { \
          return TASK_YIELDED; \
        } \
      } while(0)
    // 退出任务
    #define TASK_EXIT(s)            (s)->line = 0; return TASK_EXITED
    // 调度任务
    #define TASK_SCHEDULE(task_func)   ((task_func)
    使用上述宏,我们可以这样实现一个任务:
    // 资源管理功能
    staticint resource_count = 8;
    staticint data_items = 0;
    static int resource_available() {
    return resource_count > 0;
    }
    static void consume_resource() {
      resource_count--;
    }
    static void process_data() {
    printf("Processing data item %d
    ", data_items++);
    }
    static void release_resource() {
      resource_count++;
    }
    // 任务实现
    static char task_function(struct task_state *state) {
    staticint counter;
      TASK_BEGIN(state);
    for (counter = 0; counter 32; ++counter) {
        TASK_WAIT_UNTIL(state, resource_available());
        consume_resource();
        process_data();  // 核心业务逻辑
        // 模拟耗时操作,主动让出CPU
        TASK_YIELD(state);
        release_resource();
      }
      TASK_END(state);
    }
    // 主循环
    int main() {
    struct task_state state;
      TASK_INIT(&state);
    while (TASK_SCHEDULE(task_function(&state))) {
        // 可以执行其他任务或休眠一段时间
        usleep(10);
      }
    return0;
    }
    下面深入介绍几个关键地方。
    行号作为恢复点这里使用了LINE宏来获取当前行号作为恢复点标识。这可以确保每个恢复点都有唯一的标识符,不需要手动分配。
    当宏被预处理器展开时,LINE会被替换为实际的行号,例如:
    TASK_WAIT_UNTIL(state, condition);
    可能会被展开为:
    do {
      (state)->line = 42;  // 假设这是第42行
    case 42:;
      if(!(condition)) {
        return TASK_WAITING;
      }
    } while(0);
    性能分析这种无栈协程实现的主要优势是极低的内存开销:
  • 每个协程仅需2字节:只需存储当前执行的行号
  • 无需独立栈空间:传统线程可能需要KB~MB级别的栈空间
  • 无堆内存使用:不需要动态内存分配[/ol]Protothreads的实现在掌握了基本原理后,可以构建一个更完善的无栈协程库,这里主要介绍Protothreads的设计思路和实现(上面主要也是Protothreads的设计)
    Protothreads正是这样一个设计精巧的无栈协程库:
    typedef unsigned short lc_t;
    struct pt {
        lc_t lc;
    };
    #define PT_WAITING 0
    #define PT_YIELDED 1
    #define PT_EXITED  2
    #define PT_ENDED   3
    /* 本地延续控制 */
    #define LC_INIT(lc)     (lc) = 0
    #define LC_SET(lc)      do { (lc) = __LINE__; case __LINE__:; } while(0)
    #define LC_RESUME(lc)   switch(lc) { case 0:
    #define LC_END(lc)      }
    /* 协程控制 */
    #define PT_INIT(pt)          LC_INIT((pt)->lc)
    #define PT_THREAD(name_args) static char name_args
    #define PT_BEGIN(pt)         { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)
    #define PT_END(pt)           LC_END((pt)->lc); PT_YIELD_FLAG = 0; \
                                 PT_INIT(pt); return PT_ENDED; }
    #define PT_WAIT_UNTIL(pt, condition) \
      do { \
        LC_SET((pt)->lc); \
        if(!(condition)) return PT_WAITING; \
      } while(0)
    #define PT_YIELD(pt) \
      do { \
        PT_YIELD_FLAG = 0; \
        LC_SET((pt)->lc); \
        if(PT_YIELD_FLAG == 0) return PT_YIELDED; \
      } while(0)
    #define PT_EXIT(pt)          do { PT_INIT(pt); return PT_EXITED; } while(0)
    #define PT_SCHEDULE(f)       ((f)
    /* 信号量实现 */
    struct pt_sem {
        unsignedint count;
    };
    #define PT_SEM_INIT(s, c)    (s)->count = c
    #define PT_SEM_WAIT(pt, s) \
      do { \
        PT_WAIT_UNTIL(pt, (s)->count > 0); \
        --(s)->count; \
      } while(0)
    #define PT_SEM_SIGNAL(pt, s) ++(s)->count
    这是一个典型的生产者-消费者的应用:
    /* 示例协程 */
    PT_THREAD(producer(struct pt *pt, struct pt_sem *sem)) {
        staticint item = 0; // 必须使用静态变量[3,6](@ref)
        PT_BEGIN(pt);
       
        for(;;) {
            PT_SEM_WAIT(pt, sem);
            printf("Produced item %d
    ", item++);
            PT_SEM_SIGNAL(pt, sem);
            PT_YIELD(pt);
        }
        PT_END(pt);
    }
    PT_THREAD(consumer(struct pt *pt, struct pt_sem *sem)) {
        staticint last = -1;
        PT_BEGIN(pt);
       
        for(;;) {
            PT_SEM_WAIT(pt, sem);
            printf("Consumed item %d
    ", last++);
            PT_SEM_SIGNAL(pt, sem);
            PT_YIELD(pt);
        }
        PT_END(pt);
    }
    int main() {
        struct pt pt_prod, pt_cons;
        struct pt_sem sem = {1}; // 二进制信号量初始化[2,6](@ref)
       
        PT_INIT(&pt_prod);
        PT_INIT(&pt_cons);
        while(PT_SCHEDULE(producer(&pt_prod, &sem)) |
              PT_SCHEDULE(consumer(&pt_cons, &sem))) {
            printf("--- Scheduler Tick ---
    ");
        }
        return0;
    }
    点击此链接查看运行结果:https://godbolt.org/z/zYT6bon36
    这里我们就是通过巧妙使用C语言的switch-case语句和宏,在不增加额外内存开销的情况下,实现类似线程的顺序控制流。
    Protothreads源代码下载:https://dunkels.com/adam/pt/publications.html

    现在不少朋友都在准备校招,常规的技术学习只是提高了代码能力,还没有提升从 0 到 1 整体做项目和解决问题的能力!
    为此我们特别推出了C++训练营,带着你从 0 到 1 做 C++ 项目(你可以从项目池中任选项目),帮助你提升做项目的能力,提升从0到1的能力,熟悉做项目的完整流程,比如开发环境、编译脚本、架构设计、框架搭建、代码发布、问题调试、单元测试
    除了上面的,还有需求分析、项目规划、架构设计、任务拆解、时间规划、版本管理。另外做项目的过程中必然会遇到种种问题,可以逐步提升你的调试能力,分析问题的能力,掌握更多的调试手段。
    遇到棘手的问题,我们还有专门的导师1V1答疑解惑,给出具体建议。
    项目池里面的项目,是导师团队花费大量时间完成的,不仅有完整的代码及清晰的注释还有详细的文档和视频,并且有专门的项目导师答疑,完全不用担心自己学不会。
    相信我,这些项目绝对能够让你进步巨大!下面是其中某三个项目的说明文档
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表