【源码讲解】sylar服务器框架----协程模块

        协程就是用户线程,由用户调用,操作系统无法感知到用户线程,用户可以完全控制调度器。

        对于协程的介绍,请观看一下视频及文章,本文不再对协程的概念进行详细讲解。

        【协程第一话】协程到底是怎样的存在?_哔哩哔哩_bilibili

        【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili

        C++ 协程的近况、设计与实现中的细节和决策 - 简书

        【协程革命】理论篇!扫盲,辟谣一条龙!全语言通用,夯实基础,准备起飞! 全程字幕_哔哩哔哩_bilibili

协程学习(对称和非对称) - 知乎

ucontext_t介绍

        ucontext_t是一个结构体变量,其功能就是通过定义一个ucontext_t来保存当前上下文信息的。

// 上下文结构体定义
// 这个结构体是平台相关的,因为不同平台的寄存器不一样
// 下面列出的是所有平台都至少会包含的4个成员
typedef struct ucontext_t {
    // 当前上下文结束后,下一个激活的上下文对象的指针,只在当前上下文是由makecontext创建时有效
    struct ucontext_t *uc_link;
    // 当前上下文的信号屏蔽掩码
    sigset_t          uc_sigmask;
    // 当前上下文使用的栈内存空间,只在当前上下文是由makecontext创建时有效
    stack_t           uc_stack;
    // 平台相关的上下文具体内容,包含寄存器的值
    mcontext_t        uc_mcontext;
    unsigned long int uc_flags;
    long int uc_filler[5];
} ucontext_t;
 
// 获取当前的上下文
int getcontext(ucontext_t *ucp);
 
// 恢复ucp指向的上下文,这个函数不会返回,而是会跳转到ucp上下文对应的函数中执行,相当于变相调用了函数
int setcontext(const ucontext_t *ucp);
 
// 修改由getcontext获取到的上下文指针ucp,将其与一个函数func进行绑定,支持指定func运行时的参数,
// 在调用makecontext之前,必须手动给ucp分配一段内存空间,存储在ucp->uc_stack中,这段内存空间将作为func函数运行时的栈空间,
// 同时也可以指定ucp->uc_link,表示函数运行结束后恢复uc_link指向的上下文,
// 如果不赋值uc_link,那func函数结束时必须调用setcontext或swapcontext以重新指定一个有效的上下文,否则程序就跑飞了
// makecontext执行完后,ucp就与函数func绑定了,调用setcontext或swapcontext激活ucp时,func就会被运行
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
 
// 恢复ucp指向的上下文,同时将当前的上下文存储到oucp中,
// 和setcontext一样,swapcontext也不会返回,而是会跳转到ucp上下文对应的函数中执行,相当于调用了函数
// swapcontext是sylar非对称协程实现的关键,线程主协程和子协程用这个接口进行上下文切换
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

首先确认一些概念:

        子协程:子协程用于具体处理逻辑,被协程调度器或当前线程的主协程所管理。

        线程的主协程:当前线程的第一个协程,在当前线程使用构造函数创建出第一个子协程之前,必须手动调用sylar::Fiber::GetThis();来创建主协程。

        调度协程:指后面实现的调度器的协程,调度协程负责选出下一个要执行的协程,具体实现在协程调度模块讲解。

Fiber对于协程相关处理的概述

        协程栈采用最简单的静态栈,也就是固定大小的栈,这么写的原因只是因为它比较好写,它的缺点是分配的栈的大小比较大会浪费内存,分配的小会很容易栈溢出,不灵活。以后改造成共享栈会更好一些。

1.如何创建一个子协程?

        构造函数传入协程执行函数,想要分配的栈的大小。首先调用malloc分配一块内存(malloc不一定从堆区分配,小于128kb调用brk从堆区分配内存,大于128kb调用mmap从文件映射区分配内存)。接着调用getcontext函数,获取当前运行的程序的上下文,然后将协程栈的大小,协程栈的首地址,下一个执行的协程的地址,分别赋值给ucontext_t .uc_stack.ss_size, ucontext_t.uc_stack.ss_sp,ucontext_t.uc_link。然后调用makecontext,将刚才创建的ucontext_t和Fiber类中的静态成员函数MainFunc绑定,子协程创建完成,等待调度器调度就能执行了。

2.协程是如何参与调度的?

        sylar服务器框架的协程调度采用星切调度。由调度协程选出下一个执行的子协程。因此需要有两个线程局部变量存指向Fiber类的指针,分别指向当前运行的协程(可能是用于协程也可能是线程主协程)t_thread_fiber和线程主协程(永远都指向线程主协程)t_fiber。如果在构造子协程的时候,第三个参数设置为true,就是被调度器所调度,如果设置为false,就是被线程主协程所调度。

3.什么是协程的yield和resume操作

        协程不能参加操作系统进行的抢占式调度,只能程序员手动控制调度执行哪个协程。这是两个原子操作。

        yield操作就是当前协程让出执行权,保存当前上下文到ucontext_t中。

        resume操作就是执行当前协程,从ucontext_t取出上下文,恢复调用现场开始执行。 

4.协程状态

        正如线程运行的时候有多种状态一样,协程也应该判断它的状态。sylar的协程有三种状态,分别是“就绪态”,“运行态”,“结束态”。

        就绪态:协程刚刚创建完或协程刚刚yield,等下下一次调度执行的时候处于这个状态中。

        运行态:调用resume后,协程执行中的状态。

        结束态:协程执行完毕后,等待回收协程资源的状态。

【源码讲解】sylar服务器框架----协程模块_第1张图片

5.有栈协程和无栈协程的区别

        什么是有栈协程?什么是无栈协程?

        有栈协程有自己的执行栈,可以进行yield和resume操作。无栈协程没有自己的执行栈,不能进行yield和resume操作,也就是说不能保存和恢复上下文。注意:无栈协程不是说没有执行栈,而是没有自己的执行栈,它是用的是共享的执行栈。

        优缺点比较:有栈协程可以保存和恢复完整的函数调用历史,包括局部变量、函数参数等,可以递归调用,但是会占用较多的内存。无栈协程不能保存函数上下文,不可以递归调用,但是占用的内存较少。

6.其他

        每个协程必须有唯一的id,这个通过静态全局原子变量进行管理(所有线程共享这个变量)。有两个全局静态的原子变量,类型是uint64_t,用于生成协程id(s_fiber_id)和统计当前协程数(s_fiber_id)。

栈内存分配器:

        为了方便以后改造接口,所以通过栈内存分配器来分配内存。

class MallocStackAllocator {
public:
    static void *Alloc(size_t size) { return malloc(size); }
    static void Dealloc(void *vp, size_t size) { return free(vp); }
};

using StackAllocator = MallocStackAllocator;

 Fiber的构造函数

        首先介绍有参构造函数。

        有参构造函数用于创建子协程,参数一共有三个,分别是协程入口函数,栈的大小,本协程是否允许参与调度器调度(默认为true)(关于协程调度器的讲解在下一篇)。首先初始化协程id,s_fiber_count自增1。首先使用栈内存分配器分配一块指定大小的内存(如果参数没有传入栈的大小的话,默认128k)。然后调用getcontext函数,获取当前运行的程序的上下文,然后将协程栈的大小,协程栈的首地址,下一个执行的协程的地址,分别赋值给ucontext_t .uc_stack.ss_size, ucontext_t.uc_stack.ss_sp,ucontext_t.uc_link。然后调用makecontext,将刚才创建的ucontext_t和Fiber类中的静态成员函数MainFunc绑定,子协程创建完成,构造函数执行结束。

Fiber::Fiber(std::function cb, size_t stacksize, bool run_in_scheduler)
    : m_id(s_fiber_id++)
    , m_cb(cb)
    , m_runInScheduler(run_in_scheduler) {
    ++s_fiber_count;
    m_stacksize = stacksize ? stacksize : g_fiber_stack_size->getValue();
    m_stack     = StackAllocator::Alloc(m_stacksize);

    if (getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }

    m_ctx.uc_link          = nullptr;
    m_ctx.uc_stack.ss_sp   = m_stack;
    m_ctx.uc_stack.ss_size = m_stacksize;

    makecontext(&m_ctx, &Fiber::MainFunc, 0);

    SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber() id = " << m_id;
}

        无参构造函数用于创建线程主协程。首先将t_fiber设置为this指针,接着调用getcontext函数,获取当前运行的程序的上下文。(调用getcontext函数后,ucontext_t的协程栈就是创建Fiber的那个栈),然后修改s_fiber_count自增一,初始化协程id,结束调用。

Fiber::Fiber() {
    SetThis(this);
    m_state = RUNNING;

    if (getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }

    ++s_fiber_count;
    m_id = s_fiber_id++; // 协程id从0开始,用完加1

    SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber() main id = " << m_id;
}

子协程执行的函数MainFunc:

        当子协程被第一次调用的时候,首先调用GetThis函数。在GetThis函数中,获取t_fiber(当前正在运行的协程),然后运行子协程的入口函数,在函数中执行业务逻辑,同时也会出现一些yield和resume的操作。函数执行结束后,会清空入口函数的function,设置协程状态为结束态,然后需要手动将当前t_fiber的引用计数减一,这是因为之前调用GetThis函数的时候,的shared_from_this方法会让引用计数加一,为了能让他正确的被智能指针管理,需要手动减少引用计数,然后调用yield函数。

void Fiber::MainFunc() {
    Fiber::ptr cur = GetThis(); // GetThis()的shared_from_this()方法让引用计数加1
    SYLAR_ASSERT(cur);

    cur->m_cb();
    cur->m_cb    = nullptr;
    cur->m_state = TERM;

    auto raw_ptr = cur.get(); // 手动让t_fiber的引用计数减1
    cur.reset();
    raw_ptr->yield();
}

yield函数:

        首先判断当前状态是否处于运行态或结束态(结束态只有当协程入口函数运行完毕后,再调用yield的时候才会处于结束态),若处于就绪态则报错(运行都没运行咋调用yield让出执行权?)。然后调用SetThis方法,将t_fiber(当前正在运行的协程)设置为t_thread_fiber(线程主协程)。然后判断,若处于运行态,则子协程需要重新设置为就绪态。接着判断,若协程参与调度器调度,则保存当前上下文,并且恢复调度器主协程的上下文。如果不参与,则保存当前上下文,与调度线程切换上下文。

void Fiber::yield() {
    /// 协程运行完之后会自动yield一次,用于回到主协程,此时状态已为结束状态
    SYLAR_ASSERT(m_state == RUNNING || m_state == TERM);
    SetThis(t_thread_fiber.get());
    if (m_state != TERM) {
        m_state = READY;
    }

    // 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
    if (m_runInScheduler) {
        if (swapcontext(&m_ctx, &(Scheduler::GetMainFiber()->m_ctx))) {
            SYLAR_ASSERT2(false, "swapcontext");
        }
    } else {
        if (swapcontext(&m_ctx, &(t_thread_fiber->m_ctx))) {
            SYLAR_ASSERT2(false, "swapcontext");
        }
    }
}

resume函数:

        首先判断当前状态是否处于就绪态,若不是就报错(运行态的协程不需要调用resume切换到到执行态)。然后调用SetThis方法,将当前协程设置为正在运行的协程。若协程参与调度器调度,则保存当前上下文到调度器主协程的上下文,并且恢复子协程上下文。如果不参与,则保存当前上下文到调度线程,并恢复子协程上下文。

void Fiber::resume() {
    SYLAR_ASSERT(m_state != TERM && m_state != RUNNING);
    SetThis(this);
    m_state = RUNNING;

    // 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
    if (m_runInScheduler) {
        if (swapcontext(&(Scheduler::GetMainFiber()->m_ctx), &m_ctx)) {
            SYLAR_ASSERT2(false, "swapcontext");
        }
    } else {
        if (swapcontext(&(t_thread_fiber->m_ctx), &m_ctx)) {
            SYLAR_ASSERT2(false, "swapcontext");
        }
    }
}

reset函数:

        这个函数的作用是重置子协程。只有结束态的协程才可以重置。具体创建过程参考有参构造函数即可。

void Fiber::reset(std::function cb) {
    SYLAR_ASSERT(m_stack);
    SYLAR_ASSERT(m_state == TERM);
    m_cb = cb;
    if (getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }

    m_ctx.uc_link          = nullptr;
    m_ctx.uc_stack.ss_sp   = m_stack;
    m_ctx.uc_stack.ss_size = m_stacksize;

    makecontext(&m_ctx, &Fiber::MainFunc, 0);
    m_state = READY;
}

析构函数:

        首先s_fiber_count减一,若m_stack不为nullptr,说明有自己的栈,也就是说是子协程。首先判断是否处于结束态,然后释放栈空间。若没有自己的栈,就是线程的主协程,首先判断是否没有协程入口函数且主协程处于执行状态,然后将t_fiber设置为nullptr,结束执行。

Fiber::~Fiber() {
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::~Fiber() id = " << m_id;
    --s_fiber_count;
    if (m_stack) {
        // 有栈,说明是子协程,需要确保子协程一定是结束状态
        SYLAR_ASSERT(m_state == TERM);
        StackAllocator::Dealloc(m_stack, m_stacksize);
        SYLAR_LOG_DEBUG(g_logger) << "dealloc stack, id = " << m_id;
    } else {
        // 没有栈,说明是线程的主协程
        SYLAR_ASSERT(!m_cb);              // 主协程没有cb
        SYLAR_ASSERT(m_state == RUNNING); // 主协程一定是执行状态

        Fiber *cur = t_fiber; // 当前协程就是自己
        if (cur == this) {
            SetThis(nullptr);
        }
    }
}

协程库用法举例:

#include "sylar/sylar.h"
#include 
#include 

sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();

void run_in_fiber2() {
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber2";
}

void run_in_fiber() {
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber begin";
    sylar::Fiber::GetThis()->yield();
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber end";
    // fiber结束之后会自动返回主协程运行
}

void test_fiber() {
    // 初始化线程主协程
    sylar::Fiber::GetThis();
    sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber, 0, false));
    SYLAR_LOG_INFO(g_logger) << "use_count:" << fiber.use_count(); // 1
    fiber->resume();
    SYLAR_LOG_INFO(g_logger) << "fiber status: " << fiber->getState(); // READY
    fiber->resume();
    SYLAR_LOG_INFO(g_logger) << "fiber status: " << fiber->getState(); // TERM
    fiber->reset(run_in_fiber2); // 上一个协程结束之后,复用其栈空间再创建一个新协程
    fiber->resume();
}

int main(int argc, char *argv[]) {
    std::vector thrs;
    for (int i = 0; i < 2; i++) {
        thrs.push_back(sylar::Thread::ptr(
            new sylar::Thread(&test_fiber, "thread_" + std::to_string(i))));
    }

    for (auto i : thrs) {
        i->join();
    }
    return 0;
}

你可能感兴趣的:(java,算法,开发语言)