高性能分布式网络服务器--协程模块

协程模块

关于协程的第一篇文章发表于1963年,有兴趣的同学可以参考这里

首先本章内容只讨论协程本身,不涉及调度器。在此,建议初次接触协程的同学,不要深入到x86/64体系架构和汇编语言层面来了解协程的上下文切换,只需要知道什么是协程,什么是上下文以及上下文如何切换。

协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。 ---- 维基百科

可以通俗一点理解,协程就是看上去和使用起来的都比较特殊的函数。协程在创建的时候都需要指定一个函数入口,这一点可以类比线程。但是协程与函数的区别在于,函数一旦被调用就会从头执行到函数结束,而协程可以在执行途中退出,后续又从退出处恢复继续执行。

我们都知道CPU核的基本调度单元是线程,一个线程指的是进程中一个单一顺序的控制流,一个进程中可以有多个线程,当系统具有多个CPU核时,这些线程可以实现并行。同样一个线程可以有多个协程,但即便在多核CPU下这些协程也是串行无法并行执行。所以协程与进程和线程不属于一个维度的概念

进程线程协程关系图

协程之所以能在执行途中退出,后续又能从退出处恢复继续执行,是因为协程在中途退出时,保存了退出时间点函数执行状态,这个状态就是协程上下文。协程上下文包含了函数在当前执行状态下的全部CPU寄存器的值,这些寄存器的值记录了函数栈帧、代码的执行位置等信息,如果将这些寄存器的值重新设置给CPU,就相当于重新恢复了函数的运行

github

https://github.com/huxiaohei/tiger.git

上下文切换

进程、线程、协程都涉及到上下文切换的问题,但是三者上下文切换有很大差异

切换者切换时机切换内容切换内容的保存处切换过程切换成本
进程操作系统系统策略,开发人员无感知页全局目录、内核栈、硬件上下文内核栈用户态-内核态-用户态最高
线程操作系统系统策略,开发人员无感知内核栈、硬件上下文内核栈用户态-内核态-用户态
协程开发人员开发人员,手动控制硬件上下文自定义变量(栈/堆)用户态

实现

tiger中的协程模块是基于ucontext实现。因此在看协程模块代码之前必须对ucontext_t的结构和相关函数非常熟悉

ucontext_t

  • ucontext_t的具体结构和平台有关,因为不同平台的寄存器存在一定差异。但不管哪个平台都具备下面四个字段

    typedef struct ucontext_t {
        struct ucontext_t *uc_link; // 当前上下文结束后,下一个激活的上下文对象的指针,只在当前上下文是由makecontext创建时有效
        stack_t uc_stack;           // 当前上下文使用的栈内存空间
        sigset_t uc_sigmask;        // 当前上下文信号屏蔽掩码
        mcontext_t uc_mcontext;     // 当前上下文寄存器信息
    } ucontext_t;
    
  • 获取当前函数上下文

    int getcontext(ucontext_t *ucp);
    
  • 恢复上下文,此函数不会返回,会直接跳转到ucp所对于的函数执行

    int setcontext(const ucontext_t *ucp);
    
  • 创建上下文,此函数会将ucpfunc进行绑定,并且支持指定执行func所需要的参数。但是在调用makecontext之前必须手动开辟一个空间,并使用ucpuc_stack指定指向这块内存,这一内存空间将作为func执行所需要的内存空间。ucp中的uc_link如果不指定,那么在协程结束时,必须调用setcontextswapcontext重新指定一个有效的上下文,否则程序将跑飞

    void makecontext(ucontext_t *ucp, void (*)(void)func, int argc, ...);
    
  • 切换上下文,此函数与setcontext类似。都不会返回,而是直接跳转到ucp所对应的函数开始执行,但swapcontext会将当前函数的上下文保存到oucp

    int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
    

非对称协程模型

tiger采用非对称协程模型,每个线程都只有一个主协程,所有的子协程只可以与主线程交互,即yield退到主协程和在主协程中被resume

主协程 子协程1 子协程2 resume running yeild resume running yeild resume running yeild 主协程 子协程1 子协程2

协程状态

tiger中协程一共定义了5个状态,协程在刚创建时处于INIT状态;只有协程处于INITYIELD状态时,协程才能被resume;只有协程处于RUNNING状态时,协程才能被yield;如果协程处于TERMINAL状态,说明协程已经结束并且可以被回收使用;如果协程处于EXCEPT状态,说明执行创建协程时所指定的函数执行出错,此协程也可以被回收使用

enum State {
    INIT = 0b1,             // 协程刚创建或者被重置
    RUNNING = 0b10,         // 正在运行
    YIELD = 0b100,          // 被挂起
    TERMINAL = 0b1000,      // 运行结束
    EXCEPT = 0b10000        // 运行出错
};

创建协程

协程类虽然提供了无参默认构造函数,但是作为使用者都应该使用带参数的构造函数。构造协程时,会检测当前线程是否存在主协程,如果存在直接构造新的协程对象;如果不存在会先使用无参构造函数构造主协程,并使用线程变量s_t_main_co指向主协程

注意在构造函数中调用makecontext时,指定的函数并不是构造函数参数中的函数,而是协程类的一个静态方法Run,我们只是将构造函数参数中的函数保存在了m_fn属性中。因此在这个上下文为激活的时候,会执行Run方法,而我们就是在Run方法里面执行使用者在构造协程对象时所指定的函数

这样封装有很多好处。首先,可以降低使用者对协程对象的理解成本。使用者不需要关心协程对象的状态是如何切换,以及函数执行完后如何切换到主协程和函数执行出错的容错处理。其次,在性能测试时也很方便的统计每个协程的执行时间等

Coroutine::Coroutine(std::function<void()> fn, size_t stack_size)
    : m_fn(fn), m_state(State::INIT) {
    if (!s_t_main_co) {
        s_t_main_co = std::make_shared<Coroutine>();
        s_t_running_co = s_t_main_co;
    }
    m_id = ++s_co_id;
    m_stack_size = stack_size == 0 ? g_co_stack_size->val() : stack_size;
    m_stack = MallocStackAllocator::Alloc(m_stack_size);
    if (getcontext(&m_ctx)) {
        TIGER_ASSERT_WITH_INFO(false, "[getcontext fail]");
    }
    m_ctx.uc_stack.ss_sp = m_stack;
    m_ctx.uc_stack.ss_flags = 0;
    m_ctx.uc_stack.ss_size = m_stack_size;
    makecontext(&m_ctx, &Coroutine::Run, 0);
    ++s_co_cnt;
}

唤醒协程

tiger中提供了静态方法Resume和对象方法resume,分别来唤醒主协程和协程对象。其实静态方法Resume只是将当前线程的主协程s_t_main_co作为普通协程对象调用了对应的resume

协程对象只有处于INITYIELD时才能被唤醒,并且子协程只能在主协程中被唤醒,即resume只能在主协程中调用。因此在唤醒子协程之前,我们将当前线程的主协程(同时也是正在运行的协程)状态修改为YIELD,然后将s_t_running_co(线程变量,用于保存当前线程正在执行的协程)指向我们要唤醒的协程对象。最后调用swapcontext保存主协程的ucontext_t并切换到子协程的ucontext_t。当然,这里要处理如果协程切换失败,我们需要恢复主协程的执行状态到RUNNINGs_t_running_co重新指向主协程

void Coroutine::resume() {
    TIGER_ASSERT_WITH_INFO(m_state & (State::INIT | State::YIELD), "[coroutine can only be resumed when it is INIT or Yield]");
    s_t_main_co->m_state = State::YIELD;
    m_state = State::RUNNING;
    s_t_running_co = shared_from_this();
    if (swapcontext(&(s_t_main_co->m_ctx), &m_ctx)) {
        TIGER_ASSERT_WITH_INFO(false, "[swapcontext fail]");
    }
    s_t_main_co->m_state = State::RUNNING;
    s_t_running_co = s_t_main_co;
}

执行协程

在创建协程的时候我们将ucontext_t保存在协程对象的m_ctx属性中,并与静态方法Run绑定在一起。在唤醒协程的时候,我们会激活保存在协程对象上m_ctx属性所指向的ucontext_t。因此,协程对象一旦被唤醒,程序就会跳转到Run方法中执行,而在静态方法Run中会调用m_fn所指向的函数

一旦m_fn所指向的函数执行完,就会将协程对象的状态修改为TERMINAL;如果m_fn所指向的函数在执行过程中出错,就会将协程对象的状态修改为EXCEPT。最终,在静态方法Run结束前会调用swapcontext切回到主协程执行

void Coroutine::Run() {
    try {
        s_t_running_co->m_fn();
        s_t_running_co->m_fn = nullptr;
        s_t_running_co->m_state = State::TERMINAL;
    } catch (const std::exception &e) {
        s_t_running_co->m_state = State::EXCEPT;
        TIGER_LOG_E(tiger::SYSTEM_LOG) << "[run coroutine fail"
                                       << " id:" << s_t_running_co->m_id
                                       << " error:" << e.what() << "]";
    } catch (...) {
        s_t_running_co->m_state = State::EXCEPT;
        TIGER_LOG_E(tiger::SYSTEM_LOG) << "[run coroutine fail"
                                       << " id:" << s_t_running_co->m_id << "]";
    }
    swapcontext(&(s_t_running_co->m_ctx), &(s_t_main_co->m_ctx));
}

挂起协程

与唤醒协程类似,只是从子协程切换到主协程

void Coroutine::yield() {
    TIGER_ASSERT_WITH_INFO(s_t_running_co != s_t_main_co, "[the main coroutinue cannot be suspended by itself]");
    m_state = State::YIELD;
    s_t_main_co->m_state = State::RUNNING;
    s_t_running_co = s_t_main_co;
    if (swapcontext(&m_ctx, &(s_t_main_co->m_ctx))) {
        TIGER_ASSERT_WITH_INFO(false, "[swapcontext fail]");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值