美文网首页
操作系统——协程

操作系统——协程

作者: wipping的技术小栈 | 来源:发表于2022-05-14 21:58 被阅读0次

    一、前言

    自从换工作以后,已经少有业务学习技术的时间了,对于大的知识点的积累和更新变得比较困难。而本文的知识点——了解协程 适合作为一个在紧凑的工作生活中学习的技术。协程本身不难,但是要了解其中精髓并写出好用的协程应用是需要花费一定时间的。本文将初步的讲解协程的基础知识,并结合一段开源代码进行讲解,最后会通过修改代码完成一定的功能修改,尽量使读者可以基本了解协程

    本文前置知识:

    1. 进程
    2. 线程
    3. 上下文切换

    二、正文

    2.1 传统调用

    在了解 协程 之前,我们需要了解一下传统执行流中是如何完成我们的任务的。
    通常来说,函数 或者 称为 子程序、子例程 都是层级调用的,比如 下面的伪代码中:

    int A()
    {
        a()
    }
    int func()
    {
        A()
        B()
        C()
    }
    

    如果上述代码使用 线程 进行调用函数 func(),那么就会是:

    1. 调用 A(),进入 A() 执行 a() ,然后返回
    2. 调用 B() 后返回
    3. 调用 C() 返回

    上述函数如果在执行过程中被系统切换出去,回来继续从原来的地方继续执行。其 调用顺序是明确的

    2.2 协程调用

    结合 进程线程 ,其各自的基本概念如下:

    1. 进程:指计算机中已运行的 程序程序 是 指令和数据及其组织形式的描述。在以前的描述描述中通常会有 进程是操作系统资源调度的基本单位,但在现在的操作系统中,进程不再是执行的基本单位,而是 线程的容器。相较于 线程协程进程 拥有最多的资源,比如 文件内存
    2. 线程:是 操作系统资源调度的基本单位。一个线程是一个进程中的一个单一执行流。统一进程中的线程共享该进程的所有资源
    3. 协程:又称为 用户态线程,用于完成 协作式多任务子程序,允许执行被挂起与被恢复。其本质还是 子例程的上下文

    协程可以这样通俗的理解,即 多个函数可以在一个线程中被平等调用,这里的 平等调用 是允许 挂起和恢复,不是传统意义上的函数调用。也就是函数可以在执行到一半的时候,在适当的时机切出去完成其他函数,而这都是在同一个线程中完成。

    需要注意的的时:
    1. 通常 协程 主动在合适的时机通过主动让出的方式让其他协程得以执行
    2. 如果没有主动让出并完成后退出函数。会按照调度算法调度下一个协程执行
    3. 具体如何完成协程的退出需要看具体实现

    使用代码说明的话,其更像是:

    int func()
    {
        A()/B()/C()
    }
    

    上述这段奇怪的代码表示的是:

    1. func() 中可以即执行 A(),也执行 B()C(),他们的之间没有明确的调用顺序
    2. A() 执行到中途时,可以在适当的时机切换为执行 B()C()
    3. B()C() 执行完成或者再某个适当的时机再切换为执行 A()
    4. B()C() 同理

    以上的过程是在同一个线程中完成,不会涉及到线程的切换,减少了系统开销。

    熟悉 GO语言 的读者应该对协程会比较熟悉,在 高IO 场景下,协程可以在 低系统切换开销 的情况下有效的执行多个 子程序,从而优化整体性能。

    2.3 代码示例

    笔者使用一个比较简单的开源协程库进行说明,以便读者理解协程。
    前提说明:

    1. 该代码使用 POSIXucontext函数族 进行实现,读者可以自行查阅该函数族的相关资料。
    2. 在该代码中可以简单理解 ucontext函数族 提供了 上下文切换的条件

    下面的协程的实现讲解

    /*---------------------coroutine.h---------------------*/
    #ifndef C_COROUTINE_H
    #define C_COROUTINE_H
    
    #define COROUTINE_DEAD 0
    #define COROUTINE_READY 1
    #define COROUTINE_RUNNING 2
    #define COROUTINE_SUSPEND 3
    
    struct schedule;
    
    typedef void (*coroutine_func)(struct schedule *, void *ud);
    
    struct schedule * coroutine_open(void);
    void coroutine_close(struct schedule *);
    
    int coroutine_new(struct schedule *, coroutine_func, void *ud);
    void coroutine_resume(struct schedule *, int id);
    int coroutine_status(struct schedule *, int id);
    int coroutine_running(struct schedule *);
    void coroutine_yield(struct schedule *);
    
    #endif
    
    
    /*---------------------coroutine.c---------------------*/
    struct coroutine;
    
    /* 协程调度器 */
    struct schedule {
        char stack[STACK_SIZE]; //栈,用于协程运行时使用
        ucontext_t main;//主协程
        int nco;//协程数量
        int cap;//协程调度器允许创建的最多协程数量
        int running;//当前运行的协程id
        struct coroutine **co;//数组指针,用于指向协程实例数组
    };
    
    /* 协程 */
    struct coroutine {
        coroutine_func func; //协程执行的子程序或函数
        void *ud;//函数变量
        ucontext_t ctx;//上下文
        struct schedule * sch;//该协程所在的调度器
        ptrdiff_t cap; //协程栈的容量
        ptrdiff_t size; //协程栈的大小,一般和cap相等
        int status; //协程的状态
        char *stack;//协程的栈,用于协程保存栈使用,并不是再运行时使用,主要功能是保存协程的栈
    };
    
    /* 创建协程实例 */
    struct coroutine * 
    _co_new(struct schedule *S , coroutine_func func, void *ud) {
        /* 创建协程所需要的实例 */
        struct coroutine * co = malloc(sizeof(*co));
        /* 初始化各项基本成员 */
        co->func = func;
        co->ud = ud;
        co->sch = S;
        co->cap = 0;
        co->size = 0;
        co->status = COROUTINE_READY; 
        co->stack = NULL;
        return co;
    }
    
    /* 删除协程实例 */
    void
    _co_delete(struct coroutine *co) {
        /* 
          协程在调度过程中可能会开辟栈,所以这里需要释放,
          相关内容需要在下面的代码中才能进行讲解 
        */
        free(co->stack);
        /* 释放协程 */
        free(co);
    }
    
    /* 打开协程调度器,通过调用该函数的为主协程 */
    struct schedule * 
    coroutine_open(void) {
        /* 开辟调度器 */
        struct schedule *S = malloc(sizeof(*S));
        /* 初始化调度器成员 */
        S->nco = 0;
        S->cap = DEFAULT_COROUTINE;//DEFAULT_COROUTINE = 16
        S->running = -1;//running = -1指当前为主协程在执行
        S->co = malloc(sizeof(struct coroutine *) * S->cap);//开辟协程数组
        memset(S->co, 0, sizeof(struct coroutine *) * S->cap);//清空协程数组
        return S;
    }
    
    /* 关闭协程调度器 */
    void 
    coroutine_close(struct schedule *S) {
        int i;
        /* 删除所有协程 */
        for (i=0;i<S->cap;i++) {
            struct coroutine * co = S->co[i];
            if (co) {
                _co_delete(co);
            }
        }
        /* 删除协程数组 */
        free(S->co);
        S->co = NULL;
        /*  释放调度器 */
        free(S);
    }
    
    /* 
        创建协程 
        S :协程调度器
        func:协程需要执行的函数
        ud:函数参数
    */
    int 
    coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
        /* 创建协程实例 */
        struct coroutine *co = _co_new(S, func , ud);
        /* 
          查看当前协程的数量是否达到上限 
          如果到达上限,则扩大容量为原来的2倍,并将新创建的协程放在其中
          最后返回协程的id
        */
        if (S->nco >= S->cap) {
            int id = S->cap;
            S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
            memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
            S->co[S->cap] = co;
            S->cap *= 2;
            ++S->nco;
            return id;
        } else {
        /*
          如果协程数量没有超过上限,则将协程添加到调度器中,并返回协程的id
        */
            int i;
            for (i=0;i<S->cap;i++) {
                int id = (i+S->nco) % S->cap;
                if (S->co[id] == NULL) {
                    S->co[id] = co;
                    ++S->nco;
                    return id;
                }
            }
        }
        assert(0);
        return -1;
    }
    
    /*
        协程的主要执行函数,协程并不是执行执行其函数的,一般都在mainfunc中执行
        在协程执行前后需要做一些处理
        low32:调度器地址的低32bit
        high:调度器地址的高32bit
    */
    static void
    mainfunc(uint32_t low32, uint32_t hi32) {
        /* 获取调度器 */
        uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
        struct schedule *S = (struct schedule *)ptr;
        /* 获取当前调度需要执行的协程id,并通过id获取协程的实例 */  
        int id = S->running;
        struct coroutine *C = S->co[id];
        /* 执行协程的函数 */
        C->func(S,C->ud);
        /* 由于协程没有主动让出并退出了函数,这里需要对协程进行回收 */
        _co_delete(C);//删除协程实例
        S->co[id] = NULL;//将协程在调度器中去掉
        --S->nco;//减少调度器的协程数量
        /*
          1. 使用ucontext函数族实现时上文下切换,当前上下文退出后会切换到指定的上下文
          2. 该调度器的实现是需要从主线程切换到其他协程,所以当协程退出后会返回主协程
          3. 将当前执行的协程id修改为-1,表示当前是主协程执行
        */
        S->running = -1;
    }
    
    /* 
        恢复协程,该函数通常在主协程中使用
        S:协程调度器
        id:需要恢复执行的协程id
    */
    void 
    coroutine_resume(struct schedule * S, int id) {
        assert(S->running == -1);
        assert(id >=0 && id < S->cap);
        /* 获取需要恢复执行的协程的id并判断其有效性 */
        struct coroutine *C = S->co[id];
        if (C == NULL)
            return;
        /* 获取协程的状态,并根据状态使用不同的操作 */
        int status = C->status;
        switch(status) {
        /* 
          协程完成创建时状态为COROUTINE_READY 
          此时协程的上下文并没有完成初始化,所以需要特殊处理
        */
        case COROUTINE_READY:
            /* 初始化协程上下文 */
            getcontext(&C->ctx);
            C->ctx.uc_stack.ss_sp = S->stack;//指定协程的栈为使用调度的栈成员
            C->ctx.uc_stack.ss_size = STACK_SIZE;//指定协程栈的大小
            C->ctx.uc_link = &S->main;//指定协程退出后返回到主协程,与mainfunc中的操作相呼应
            S->running = id;//声明当前执行的协程id
            C->status = COROUTINE_RUNNING;//将协程的状态设置为RUNNING
            /* 
              使用makecontext创建协程上下文 
              1. 协程的执行函数为mainfunc
              2. 指定mainfunc的参数的数量为2,分别为调度器的指针的高低32bit
            */
            uintptr_t ptr = (uintptr_t)S;//
            makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
              /* 
                使用swapcontext完成上下文的切换
                1. 该函数会将当前的上下文保存在S->main中,该函数通常在主线程中执行,所以S->main也就保存了当前的上下文
                2. 将当前的上下文切换到C->ctx,即刚刚构造的上下文中
                3. 下一步会跳到刚刚指定的mainfunc中执行协程的func成员
              */
            swapcontext(&S->main, &C->ctx);
            break;
        /*
          协程在挂起时状态为COROUTINE_SUSPEND,此时协程已经有了上下文,但是被挂起,现在需要恢复执行
        */
        case COROUTINE_SUSPEND:
            /* 将协程的保存的栈设置到调度器S的栈成员中,因为在协程上下文初始化时指定的栈是调度器S的stack成员 */
            memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
            S->running = id;//声明当前执行的协程id为恢复执行协程的id
            C->status = COROUTINE_RUNNING;//将协程的状态设置为RUNNING
            swapcontext(&S->main, &C->ctx);//切换协程
            break;
        default:
            assert(0);
        }
    }
    
    /* 
        保存协程的栈,通常在协程的让出函数中调用,用于保存协程当前执行的栈
        C:协程
        top:协程的栈底指正,使用top表示的是地址的top,因为栈是向上增长的,所以方向反了
    */
    static void
    _save_stack(struct coroutine *C, char *top) {
        /* dummp为当前协程的栈顶变量,通过dummu可以获取当前的栈顶指针 */
        char dummy = 0;
        assert(top - &dummy <= STACK_SIZE);
        /* 如果当前的栈顶是否已经超出当前栈的容量 */
        if (C->cap < top - &dummy) {
            free(C->stack);//释放当前协程的保存栈
            C->cap = top-&dummy;//获取当前栈使用的大小
            C->stack = malloc(C->cap);//根据大小重新分配保存栈
        }
        C->size = top - &dummy;//设置保存栈大小,也就是当前栈使用的情况
        memcpy(C->stack, &dummy, C->size);//将栈保存到协程的stack成员中,在这里完成了栈的保存
    }
    
    /*
      协程让出函数,通常协程自己调用,用于当前协程让出给其他协程使用,在该实现中是让出到主协程
      S:协程调度器
    */
    void
    coroutine_yield(struct schedule * S) {
        /* 获取当前运行的协程id */
        int id = S->running;
        assert(id >= 0);
        /* 获取当前协程实例 */
        struct coroutine * C = S->co[id];
        assert((char *)&C > S->stack);
        /* 将当前的运行栈保存到协程中 */
        _save_stack(C,S->stack + STACK_SIZE);
        /* 将协程状态置为挂起 */
        C->status = COROUTINE_SUSPEND;
        /* 声明运行协程id */
        S->running = -1;
        /* 切换上下文到主协程 */
        swapcontext(&C->ctx , &S->main);
    }
    
    /* 获取指定id协程的状态 */
    int 
    coroutine_status(struct schedule * S, int id) {
        assert(id>=0 && id < S->cap);
        if (S->co[id] == NULL) {
            return COROUTINE_DEAD;
        }
        return S->co[id]->status;
    }
    
    /* 获取当前运行的协程id*/
    int 
    coroutine_running(struct schedule * S) {
        return S->running;
    }
    
    

    上面主要讲解了协程的实现,但关看实现可能还无法完全了解协程是如何运行的,此时还无法将协程的整个声明周期组织起来
    下面讲解一下该协程库的demo,方便读者理解

    #include "coroutine.h"
    #include <stdio.h>
    
    struct args {
        int n;
    };
    
    /* 协程的执行函数 */
    static void
    foo(struct schedule * S, void *ud) {
        struct args * arg = ud;
        int start = arg->n;
        int i;
        for (i=0;i<5;i++) {
          /* 打印当前协程的id和变量 */
            printf("coroutine %d : %d\n",coroutine_running(S) , start + i);
            /* 让出协程 */
            coroutine_yield(S);
        }
    }
    
    /* 测试函数 */
    static void
    test(struct schedule *S) {
        struct args arg1 = { 0 };
        struct args arg2 = { 100 };
        /* 
          1. 创建2个协程,并传入相同的执行函数 
          2. 传入不同的变量,用于区分协程
        */
        int co1 = coroutine_new(S, foo, &arg1);
        int co2 = coroutine_new(S, foo, &arg2);
        printf("main start\n");
        /* 
          1. 判断2个协程的状态,如果为COROUTINE_DEAD(0)则说明协程已经退出 
          2. 至于协程退出可以看上一章节中的mainfunc函数讲解
        */
        while (coroutine_status(S,co1) && coroutine_status(S,co2)) {
            /* 如果没有退出则恢复当前协程执行 */
            printf("step 1\n");
            coroutine_resume(S,co1);
            printf("step 2\n");
            coroutine_resume(S,co2);
            printf("step 3\n");
        } 
        printf("main end\n");
    }
    
    int 
    main() {
        struct schedule * S = coroutine_open();
        test(S);
        coroutine_close(S);
        
        return 0;
    }
    

    执行结果为:


    修改

    从demo可以看出几点:
    1. 每次执行完协程后都需要返回主协程,并由主协程继续恢复下一个协程的运行
    2. 从而可以看出该库的协程的顺序是明确的

    2.4 代码修改

    2.3章节 中我们可以了解到协程的的运行,但该实现会造成2个问题:
    1.每次协程让出都需要回到主协程才能继续下一个协程,造成不必要的切换,因为主协程本身没有做任何事情
    2. 协程的执行顺序由主协程指定,无法根据调度算法进行切换

    基于上面2个原因,笔者修改代码以实现下面2点:
    1. 协程让出或者退出后不回到主协程,由调度器查找下一个运行的协程并执行
    2. 运行的协程有调度算法决定

    下面的代码修改和讲解

    /*---------------------coroutine_v2.h---------------------*/
    #ifndef C_COROUTINE_H
    #define C_COROUTINE_H
    
    #define COROUTINE_DEAD 0
    #define COROUTINE_READY 1
    #define COROUTINE_RUNNING 2
    #define COROUTINE_SUSPEND 3
    
    struct schedule;
    
    typedef void (*coroutine_func)(struct schedule *, void *ud);
    
    struct schedule * coroutine_open(void);
    void coroutine_close(struct schedule *);
    
    /* 相比于上面版本,少了resum恢复函数,只有yield让出函数 */
    int coroutine_new(struct schedule *, coroutine_func, void *ud);
    int coroutine_status(struct schedule *, int id);
    int coroutine_running(struct schedule *);
    void coroutine_yield(struct schedule *);
    
    #endif
    
    /*---------------------coroutine_v2.c---------------------*/
    #include "coroutine.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    #include <stddef.h>
    #include <string.h>
    #include <stdint.h>
    
    #if __APPLE__ && __MACH__
        #include <sys/ucontext.h>
    #else
        #include <ucontext.h>
    #endif
    
    #define STACK_SIZE (1024*1024)
    #define DEFAULT_COROUTINE 16
    
    /* 协程 */
    struct coroutine
    {
        coroutine_func func;//协程执行函数
        void *ud;//函数变量
        ucontext_t ctx;//协程上下文
        struct schedule * sch;//协程所在的调度器
        ptrdiff_t cap;//协程栈的容量
        ptrdiff_t size;//协程栈的大小,一般于cap相等
        int status;//协程状态
        char* stack;//协程的栈,该栈是运行时使用。并不是之前是保存时使用
    };
    
    /*
        协程调度器
        可以看出和之前相比少了许多成员
    */
    struct schedule
    {
        int nco;//协程数量
        int cap;//调度器的协程最大数量
        int running;//当前运行的协程id
        ucontext_t main;//主协程上下文
        struct coroutine **co;//数组指针,指向协程数组
    };
    
    /* 删除协程 */
    void co_delete(struct coroutine *co)
    {
        /*
            1. 与之前的实现不同,这里删除协程修改为将协程的状态置为COROUTINE_DEAD
            2. 使用该状态以让协程在调度时不会被选择
        */
        co->status = COROUTINE_DEAD;
    }
    
    /* 查找下一个可以执行的协程 */
    static int find_next_co(struct schedule *S, int id)
    {
        /*
            1. 找到下一个存在切状态为挂起或者ready的协程
            2. 状态为COROUTINE_DEAD的协程不会被查找到
        */
        for (int i = id; i < S->nco; i++)
        {
            if (NULL == S->co[i])
            {
                continue;
            }
    
            if (COROUTINE_SUSPEND == S->co[i]->status || COROUTINE_READY == S->co[i]->status)
            {
                return i;
            }
        }
    
        /* 如果没有满足的协程的返回-1,回到主协程 */
        return -1;
    }
    
    /*
      协程的主要执行函数,在该函数中执行协程中的func成员
    */
    static void mainfunc(uint32_t low32, uint32_t hi32)
    {
        uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
        struct schedule *S = (struct schedule *)ptr;
        int id = S->running;
        struct coroutine *C = S->co[id];
        struct coroutine *next_C = NULL;
        C->func(S,C->ud);
        /*
          与之前的实现不同,这里对于协程退出的处理做了修改
          1. co_delete没有直接删除协程,只是修改了状态
          2. 因为这里还在协程的上下文中,此时删除协程会出错
          3. 查找下一个执行的协程
          4. 如果协程调度器没有需要执行的协程了,则直接返回主协程
        */
        co_delete(C);
        id = find_next_co(S, id);
        if (-1 == id)
        {
            /* 如果协程调度器没有需要执行的协程了,则直接返回主协程 */
            S->running = -1;
            setcontext(&S->main);
        }
        else
        {
            /* 执行下一个协程 */
            S->running = id;
            next_C = S->co[id];
            setcontext(&next_C->ctx);
        }
    }
    
    struct coroutine *co_new(struct schedule *S , coroutine_func func, void *ud)
    {
        struct coroutine* co = malloc(sizeof(*co));
        co->func = func;
        co->ud = ud;
        co->sch = S;
        co->cap = STACK_SIZE;
        co->size = STACK_SIZE;
        co->status = COROUTINE_READY;
        co->stack = malloc(STACK_SIZE);//分配协程的栈,这里主要是用于协程执行时使用
    
        /*
          初始化协程上下文
          与之前相比:
          1. 协程上下文的初始化在创建协程时完成,不在resum函数中执行
          2. 协程上下文中运行的栈改为协程的stack成员,而不是之前调度器中stack成员
          这样做的好处是不需要在调度时保存栈,直接切换上下文即可
        */
        getcontext(&co->ctx);
        co->ctx.uc_stack.ss_sp = co->stack;
        co->ctx.uc_stack.ss_size = STACK_SIZE;
        co->ctx.uc_link = &S->main;
        /* 创建协程上下文 */
        uintptr_t ptr = (uintptr_t)S;
        makecontext(&co->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
    
        return co;
    }
    
    /* 打开协程调度器,与之前的实现一样 */
    struct schedule* coroutine_open(void)
    {
        struct schedule *S = malloc(sizeof(*S));
        S->nco = 0;
        S->cap = DEFAULT_COROUTINE;
        S->running = -1;
        S->co = malloc(sizeof(struct coroutine *) * S->cap);
        memset(S->co, 0, sizeof(struct coroutine *) * S->cap);
        return S;
    }
    
    /* 关闭协程调度器 */
    void coroutine_close(struct schedule *S)
    {
        int i;
        for (i = 0; i < S->cap; i++)
        {
            struct coroutine* co = S->co[i];
            if (co)
            {
                /* 删除协程及其栈 */
                free(co->stack);
                free(co);
            }
        }
        free(S->co);
        S->co = NULL;
        free(S);
    }
    
    /* 创建协程,与之前的实现一样 */
    int coroutine_new(struct schedule *S, coroutine_func func, void *ud)
    {
        struct coroutine *co = co_new(S, func , ud);
        if (S->nco >= S->cap)
        {
            int id = S->cap;
            S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
            memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
            S->co[S->cap] = co;
            S->cap *= 2;
            ++S->nco;
            return id;
        }
        else
        {
            int i;
            for (i=0;i<S->cap;i++)
            {
                int id = (i+S->nco) % S->cap;
                if (S->co[id] == NULL)
                {
                    S->co[id] = co;
                    ++S->nco;
                    return id;
                }
            }
        }
        return -1;
    }
    
    /*
        协程让出函数
        1. 与之前的实现不同,这里的yield函数会指定下一个执行的协程
        2. 完成协程的直接切换,不需要回到主协程
    */
    void coroutine_yield(struct schedule * S)
    {
        int id = S->running;//获取当前执行协程的id
        struct coroutine *C = NULL;
        struct coroutine* next_C = NULL;
        if (0 >= S->nco)
        {
            printf("no routine create\n");
            return;
        }
    
        /* 如果当前为主协程 */
        if (-1 >= id)
        {
            id = 0;//指定默认协程
            id = find_next_co(S, id);//查找下一个可用
            if (-1 == id)
            {
                /*
                    如果找不到则返回,因为当前是主协程,所以不需要切换
                */
                printf("yidld error\n");
                return;
            }
            C = S->co[id];//获取协程的实例
            S->running = id;
            C->status = COROUTINE_RUNNING;
            swapcontext(&S->main, &C->ctx);//切换到第一个协程
        }
        else //如果当前为其他协程
        {
            C = S->co[id];//获取当前协程
            C->status = COROUTINE_SUSPEND;//将当前协程的状态修改为挂起
    
            id = find_next_co(S, id);//查找下一个可用
            if (-1 == id)
            {
                /*
                    如果找不到可执行协程,则返回主协程
                */
                setcontext(&S->main);
            }
            next_C = S->co[id];//获取下一个协程实例
            next_C->status = COROUTINE_RUNNING;//将下一个协程状态置为运行
    
            S->running = id;//设置运行协程的id
            swapcontext(&C->ctx , &next_C->ctx);//切换到下一个协程
        }
    }
    
    /* 获取指定协程状态 */
    int coroutine_status(struct schedule * S, int id)
    {
        if (S->co[id] == NULL)
        {
            return COROUTINE_DEAD;
        }
        return S->co[id]->status;
    }
    
    /* 获取当前运行的协程 */
    int coroutine_running(struct schedule * S)
    {
        return S->running;
    }
    
    

    上面讲解修改后的代码,下面我们看看具体的使用用例方便读者理解

    #include "coroutine_v2.h"
    #include <stdio.h>
    
    struct args {
        int n;
    };
    
    static void
    foo(struct schedule * S, void *ud) {
        struct args * arg = ud;
        int start = arg->n;
        int i;
        for (i=0;i<5;i++) {
            printf("coroutine %d : %d\n",coroutine_running(S) , start + i);
            coroutine_yield(S);
        }
    }
    
    static void
    test(struct schedule *S) {
        struct args arg1 = { 0 };
        struct args arg2 = { 100 };
        /* 创建2个协程 */
        int co1 = coroutine_new(S, foo, &arg1);
        int co2 = coroutine_new(S, foo, &arg2);
        printf("main start\n");
        /* 判断协程状态是否正常 */
        while (0 != coroutine_status(S,co1) || 0 != coroutine_status(S,co2)) {
            /* 让出协程 */
            printf("before yield\n");
            coroutine_yield(S);
            printf("after yield\n");
        }
        printf("main end\n");
    }
    
    int
    main() {
        struct schedule * S = coroutine_open();
        test(S);
        coroutine_close(S);
    
        return 0;
    }
    

    执行结果如下,可以看到每次调度都没有进入主协程


    修改后

    PS:以上代码作为demo进行,可能存在bug,欢迎各位读者朋友讨论

    2.5 总结

    总结一下,协程有以下优点及适用场景如下:
    优点:

    1. 在某些场景如生产消费场景,可以简化编程模型
    2. 降低切换开销,因为不需要系统中断和系统进行调度,仅有上下文开销

    适用于 高IO场景,典型的有:

    1. 高并发网络编程
    2. 高IO,比如 异构计算、磁盘IO、设备IO

    三、参考链接

    1. 参考代码
    2. 维基百科
    3. 协程 以及进程和线程
    4. 协程(coroutine)简介
    5. 上下文切换

    相关文章

      网友评论

          本文标题:操作系统——协程

          本文链接:https://www.haomeiwen.com/subject/nxhmurtx.html