[sylar]C++高性能服务器框架——协程模块

1,636 阅读7分钟

协程模块概述

概念

首先介绍一下为什么要使用协程。从了解进程,线程,协程之间的区别开始。

  1. 从定义来看

    • 进程是资源分配和拥有的基本单位。进程通过内存映射拥有独立的代码和数据空间,若没有内存映射给进程独立的空间,则没有进程的概念了。
    • 线程是程序执行的基本单位。线程都处在一个进程空间中,可以相互访问,没有限制,所以使用线程进行多任务变成十分便利,所以当一个线程崩溃,其他任何一个线程都不能幸免。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
    • 协程是用户态的轻量级线程,线程内部调度的基本单位。协程在线程上执行。
  2. 从系统调用来看

    • 进程由操作系统进行切换,会在用户态与内核态之间来回切换。在切换进程时需要切换虚拟内存空间,切换页表,切换内核栈以及硬件上下文等,开销非常大。
    • 线程由操作系统进行切换,会在用户态与内核态之间来回切换。在切换线程时需要保存和设置少量寄存器内容,开销很小。
    • 协程由用户进行切换,并不会陷入内核态。先将寄存器上下文和栈保存,等切换回来的时候再进行恢复,上下文的切换非常快
  3. 从并发性来看

    • 不同进程之间切换实现并发,各自占有CPU实现并行
    • 一个进程内部的多个线程并发执行
    • 同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理

通过协程,我们可以让程序按照我们的想法去运行,而不是从头到尾的执行下去。例如我们在执行一个函数时,可以通过yield退出,让出当前的CPU执行权,等到了合适的时候,通过resume重新恢复运行。

因为协程是在单线程上运行的,并不是并发执行的,是顺序执行的,所以不能使用锁来做协程的同步,这样会直接导致线程的死锁。

ucontext_t

协程模块基于ucontext_t实现,基本结构如下

  • ucontext_t结构体
#include <ucontext.h>
typedef struct ucontext_t {
  struct ucontext_t* uc_link;
  sigset_t uc_sigmask;
  stack_t uc_stack;
  mcontext_t uc_mcontext;
  ...
};

类成员解释: uc_link:为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。 uc_sigmask:执行当前上下文过程中需要屏蔽的信号列表,即信号掩码

uc_stack:为当前context运行的栈信息。 uc_stack.ss_sp:栈指针指向stack uc_stack.ss_sp = stack; uc_stack.ss_size:栈大小 uc_stack.ss_size = stacksize;

uc_mcontext:保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明。

  • ucontext族函数
#include <ucontext.h>
void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t* olducp, ucontext_t* newucp);
int getcontext(ucontext_t* ucp);
int setcontext(const ucontext_t* ucp);

makecontext:初始化一个ucontext_t,func参数指明了该context的入口函数,argc为入口参数的个数,每个参数的类型必须是int类型。另外在makecontext前,一般需要显示的初始化栈信息以及信号掩码集同时也需要初始化uc_link,以便程序退出上下文后继续执行。

swapcontext:原子操作,该函数的工作是保存当前上下文并将上下文切换到新的上下文运行。

getcontext:将当前的执行上下文保存在cpu中,以便后续恢复上下文

setcontext:将当前程序切换到新的context,在执行正确的情况下该函数直接切换到新的执行状态,不会返回。

注意:setcontext执行成功不返回,getcontext执行成功返回0,若执行失败都返回-1。若uc_link为NULL,执行完新的上下文之后程序结束。

实现思路

使用非对称协程的设计思路,通过主协程创建新协程,主协程由swapIn()让出执行权执行子协程的任务,子协程可以通过YieldToHold()让出执行权继续执行主协程的任务,不能在子协程之间做相互的转化,这样会导致回不到main函数的上下文。这里使用了两个线程局部变量保存当前协程和主协程,切换协程时调用swapcontext,若两个变量都保存子协程,则无法回到原来的主协程中。

Fiber::GetThis() 获得主协程
                  swapIn()        
Thread->man_fiber --------> sub_fiber (new(Fiber(cb)))
            ^
            | Fiber::YieldToHold()
            |
         sub_fiber

详解

class Fiber(协程模块)

// 用于生成协程id
static std::atomic<uint64_t> s_fiber_id {0};
// 用于统计当前的协程数
static std::atomic<uint64_t> s_fiber_count {0};
​
// 约定协程栈的大小1MB
static ConfigVar<uint32_t>::ptr g_fiber_stack_size =
    Config::Lookup<uint32_t>("fiber.stack_size", 1024 * 1024, "fiber stack size");
​
// 当前协程
static thread_local Fiber *t_fiber = nullptr;
// 主协程
static thread_local Fiber::ptr t_threadFiber = nullptr;

t_fiber:指向当前运行的协程,初始化时,指向线程主协程

t_threadFiber:指向线程的主协程,初始化时,指向线程主协程,当子协程resume时,主协程让出执行权,并保存上下文到t_threadFiberucontext_t中,同时激活子协程的ucontext_t的上下文。当子协程yield时,子协程让出执行权,从t_threadFiber获得主协程上下文恢复运行。

  • 创建/释放协程运行栈
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;
  • 协程有五种状态
enum State
{
    // 初始化
    INIT,
    // 暂停
    HOLD,
    // 执行
    EXEC,
    // 结束
    TERM,
    // 可执行
    READY,
    // 异常
    EXCEPT
};

mumber(成员变量)

// 协程id
uint64_t m_id = 0;
// 协程运行栈大小
uint32_t m_stacksize = 0;
// 协程状态
State m_state = INIT;
// 上下文
ucontext_t m_ctx;
// 协程运行栈指针
void* m_stack = nullptr;
// 协程执行方法
std::function<void()> m_cb;

Fiber(构造函数)

  • private无参构造

主协程的构造

Fiber::Fiber() {
    m_state = EXEC;
    // 设置当前协程
    SetThis(this);
    // 获取当前协程的上下文信息保存到m_ctx中
    if (getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }
​
    ++s_fiber_count;
​
    SYLAR_LOG_DEBUG(g_logger)  << "Fiber::Fiber(root)";
}
  • public有参构造

子协程的构造

Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool use_caller)
    :m_id(s_fiber_count)
    ,m_cb(cb){
    ++s_fiber_count;
    // 若给了初始化值则用给定值,若没有则用约定值
    m_stacksize = stacksize ? stacksize : g_fiber_stack_size->getValue();
	
	// 获得协程运行指针
    m_stack = StackAllocator::Alloc(m_stacksize);
	// 保存当前协程上下文信息到m_ctx中
    if (getcontext(&m_ctx)) {
        SYLAR_ASSERT2(false, "getcontext");
    }
	// uc_link为空,执行完当前context之后退出程序。
    m_ctx.uc_link = nullptr;
	// 初始化栈指针
    m_ctx.uc_stack.ss_sp = m_stack;
	// 初始化栈大小
    m_ctx.uc_stack.ss_size = m_stacksize;
	
	// 指明该context入口函数
    if (!use_caller) {
        makecontext(&m_ctx, &Fiber::MainFunc, 0);
    } else {
        makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
    }
    
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber id = " << m_id;
}

~Fiber(析构函数)

释放协程运行栈

Fiber::~Fiber() {
    --s_fiber_count;
    // 子协程
    if (m_stack) {
        // 不在准备和运行状态
        SYLAR_ASSERT(m_state == TERM || m_state == INIT || m_state == EXCEPT);
		// 释放运行栈
        StackAllocator::Dealloc(m_stack, m_stacksize);
    } else {
     	// 主协程的释放要保证没有任务并且当前正在运行
        SYLAR_ASSERT(!m_cb);
        SYLAR_ASSERT(m_state == EXEC);
		//若当前协程为主协程,将当前协程置为空
        Fiber *cur = t_fiber;
        if (cur == this) {
            SetThis(nullptr);
        }
    }
    SYLAR_LOG_DEBUG(g_logger) << "Fiber::~Fiber id = " << m_id;
}

reset(重置协程)

void Fiber::reset(std::function<void()> cb) {
    // 主协程不分配栈
    SYLAR_ASSERT(m_stack);
    // 当前协程不在准备和运行态
    SYLAR_ASSERT(m_state == INIT || m_state == TERM || m_state == EXCEPT);
    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 = INIT;
}

call、swapIn(切换到当前协程)

// 从协程主协程切换到当前协程
void Fiber::call() {
    SetThis(this);
    m_state = EXEC;
    if (swapcontext(&t_threadFiber->m_ctx, &m_ctx)) {
        SYLAR_ASSERT2(false, "swapIn_context");
    }
}
// 从调度器的主协程切换到当前协程
void Fiber::swapIn() {
    SetThis(this);
    SYLAR_ASSERT(m_state != EXEC);
    m_state = EXEC;

    if (swapcontext(&Scheduler::GetMainFiber()->m_ctx, &m_ctx)) {
        SYLAR_ASSERT2(false, "swapIn_context");
    }
}

back、swapOut(当前协程切换到后台)

// 从当前协程切换到主协程
void Fiber::back() {
    SetThis(t_threadFiber.get());
    if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}
// 从当前协程切换到调度器主协程
void Fiber::swapOut() {
    SetThis(Scheduler::GetMainFiber());
    // SYLAR_LOG_DEBUG(g_logger) << "change fiber with" << Scheduler::GetMainFiber()->GetFiberId();

    if (swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx)) {
        SYLAR_ASSERT2(false, "swapcontext");
    }
}

SetThis(设置当前协程)

void Fiber::SetThis(Fiber* f) {
    t_fiber = f;
}

GetThis(返回当前协程并获得主协程)

Fiber::ptr Fiber::GetThis() {
    // 返回当前协程
    if (t_fiber) {
        return t_fiber->shared_from_this();
    }
    // 获得主协程
    Fiber::ptr main_fiber(new Fiber);
    // 此时当前协程应该为主协程
    SYLAR_ASSERT(main_fiber.get() == t_fiber);
    t_threadFiber = main_fiber;

    return t_fiber->shared_from_this();
}

YieldToReady(协程切换到后台, 并且设置为Ready状态)

void Fiber::YieldToReady() {
    Fiber::ptr cur = GetThis();
    cur->m_state = READY;
    cur->swapOut();
}

YieldToHold(协程切换到后台, 并且设置为Hold状态)

void Fiber::YieldToHold() {
    Fiber::ptr cur = GetThis();
    cur->m_state = HOLD;
    cur->swapOut();
}

MainFunc(协程执行函数)

void Fiber::MainFunc() {
    // 获得当前协程
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur);
    try {
        // 执行任务
        cur->m_cb();
        cur->m_cb = nullptr;
        // 将状态设置为结束
        cur->m_state = TERM;
    } catch (std::exception &ex) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: " << ex.what()
                                  << std::endl
                                  << " fiber_id = " << cur->getId()
                                  << std::endl
                                  << sylar::BacktraceToString();
    }
    catch (...)
    {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: "
                                  << std::endl
                                  << " fiber_id = " << cur->getId()
                                  << std::endl
                                  << sylar::BacktraceToString();
    }
	
    // 获得裸指针
    auto raw_ptr = cur.get();
    // 引用-1,防止fiber释放不掉
    cur.reset();
    //执行完释放执行权
    raw_ptr->swapOut();

    SYLAR_ASSERT2(false, "never reach fiber_id = " + std::to_string(raw_ptr->getId()));
}

void Fiber::CallerMainFunc() {
    Fiber::ptr cur = GetThis();
    SYLAR_ASSERT(cur);
    try {
        cur->m_cb();
        cur->m_cb = nullptr;
        cur->m_state = TERM;
    } catch (std::exception &ex) {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: " << ex.what()
                                  << std::endl
                                  << " fiber_id = " << cur->getId()
                                  << std::endl
                                  << sylar::BacktraceToString();
    }
    catch (...)
    {
        cur->m_state = EXCEPT;
        SYLAR_LOG_ERROR(g_logger) << "Fiber Except: "
                                  << std::endl
                                  << " fiber_id = " << cur->getId()
                                  << std::endl
                                  << sylar::BacktraceToString();
    }

    auto raw_ptr = cur.get();
    cur.reset();
    raw_ptr->back();

    SYLAR_ASSERT2(false, "never reach fiber_id = " + std::to_string(raw_ptr->getId()));
}

总结

举个具体的例子。

  • main中创建了3个线程执行test_fiber函数,每个线程在创建时都绑定了各自的Thread::run方法,在run方法中执行test_fiber(run方法执行时会初始化线程信息:初始化t_thread,线程名称,线程id)。
  • test_fiber中首先使用sylar::Fiber::GetThis()获得主协程。
  • 通过sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber, 0, true));获得一个子协程,该协程与run_in_fiber方法绑定。
  • 当使用fiber->call()时,从当前主协程切换到子协程执行任务。在初始化子协程时,通过makecontext(&m_ctx, &fiber::CallerMainFunc)指明协程上下文入口为CallerMainFunc方法,所以切换到该方法执行run_in_fiber方法。
  • run_in_fiber中使用sylar::Fiber::Yield()让出当前协程的执行权,切换到主协程test_fiber中继续执行。
void run_in_fiber() {
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber begin";
    sylar::Fiber::Yield();
    SYLAR_LOG_INFO(g_logger) << "run_in_fiber end";
    sylar::Fiber::Yield();
}

void test_fiber() {
    SYLAR_LOG_INFO(g_logger) << "main after begin -l";
    {   
        sylar::Fiber::GetThis(); 
        SYLAR_LOG_INFO(g_logger) << "main after begin";
        sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber, 0, true));
        fiber->call();
        SYLAR_LOG_INFO(g_logger) << "main after swapIn";
        fiber->call();
        SYLAR_LOG_INFO(g_logger) << "main after end";
        fiber->call();
    }
    SYLAR_LOG_INFO(g_logger) << "main after end -l";
}

int main(int argc, char** argv) {
    sylar::Thread::SetName("main");

    std::vector<sylar::Thread::ptr> thrs;
    for (int i = 0; i < 3; ++i) {
        thrs.push_back(sylar::Thread::ptr(
            new sylar::Thread(&test_fiber, "name_" + std::to_string(i))));
    }

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

    return 0;
}