syalr-4-fiber
对sylar的协程模块进行说明
基础知识
这一章介绍sylar任务调度模块中的协程子模块。sylar是支持多线程多协程的服务器框架,什么是协程?sylar为什么要采用多线程多协程的设计呢?
什么是协程?
对协程概念的理解可以对比线程,通用的说法是协程是⼀种轻量级线程,⽤户态线程。
协程的本质就是函数和函数运行状态的组合 。协程和函数的不同之处是,函数⼀旦被调用,只能从头开始执行,直到函数执行结束退出,而协程则可以执行到⼀半就退出(称为yield),但此时协程并未真正结束,只是暂时让出CPU执行权,在后面适当的时机协程可以重新恢复运行(称为resume),在这段时间里其他的协程可以获得CPU并运⾏,所以协程被描述称为轻量级线程。
sylar为什么要采用多线程多协程的设计呢?
协程被广泛应用于异步编程,即程序在等待IO或其他事件时可以继续执行其他任务,由于其轻量级的特性,这使得协程之间的切换代价非常低,使得采用协程的应用具有极高的性能。
在服务器框架设计中,我们要处理各种IO事件,如socket的accpet,read,write等事件。这类事件通常会阻塞线程,而利用协程,采用异步编程的模型,可以极大提高系统性能,并简化上层服务设计。
另外,从线程的视角来看,各个协程的运行是串行的,这使得多个协程之间的并发控制更简单。
什么是对称协程与非对称协程?
两者的本质区别在于调度权的转移,对称协程中,在每个协程都可以切换到任意一个其他的协程,而在非对称协程中,协程之间的切换具有类似函数调用的关系,若协程A切换到协程B,那么协程B只能Yield到协程A。
Go语言的协程(go coroutine)是对称协程,而sylar的协程和腾讯的libco协程库采用的是非对称协程。
syalr的协程是非对称协程中一种非常简单的实现,存在着主协程和子协程的概念,协程的切换只发生在主协程和子协程,系统只存在一个主协程,而子协程之间不能直接切换。他们之间的切换逻辑如下如所示。
协程切换
sylar的协程实现依赖于Linux下的ucontext族函数
#include <ucontext.h>
int getcontext(ucontext_t *ucp); // 获取当前上下文
int setcontext(const ucontext_t *ucp); // 设置当前上下文
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...); // 修改ucp上下文
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp); // 保存当前上下文到oucp,然后切换到ucp
// ucontext_t保存协程上下文信息
typedef struct ucontext_t {
struct ucontext_t *uc_link; // 当前协程终止后将resume的下一个协程
sigset_t uc_sigmask; // 当前协程屏蔽的信号
stack_t uc_stack; // 当前协程的栈指针
mcontext_t uc_mcontext; // 保存的上下文
...
} ucontext_t;
通过上述接口,我们就能够很方便的创建、保存、恢复一段上下文(协程)。这里 给出了如何使用这些接口的示例,强烈建议先通读一遍。后文将假设读者有使用这些接口的基础。
协程是一个抽象的概念,具体到代码中,协程只是一段上下文信息,比如执行的地址,寄存器值等。在sylar中,我们用一个结构体Fiber来保存这些信息,因此可以说,每个Fiber对象就对应一个协程。这是理解代码的关键!
使用
协程的使用非常简单,我们通过new Fiber(cb)创建一个协程,然后执行Resume/Yield切换协程即可
我对sylar的fiber相关源码做了改动,使得其更容易理解,并且减少了代码冗余。比如SwapIn/SwapOut函数替换成更易理解的Resume/Yield,call/back相关函数与Resume/Yield合并等。相关代码在这里
#include "sylar/sylar.h"
sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
void run_in_fiber() {
SYLAR_LOG_INFO(g_logger) << "run_in_fiber begin";
// Yield和YieldToHold的区别是,前者是成员方法,后者为静态方法,后者会获取当前协程,然后调用Yield
sylar::Fiber::YieldToHold(); // -----------------> 2
SYLAR_LOG_INFO(g_logger) << "run_in_fiber end";
sylar::Fiber::YieldToHold(); // -----------------> 4
// -> MainFunc - Yield() // --------------------> 6
}
// 每个线程运行此函数
void test_fiber() {
SYLAR_LOG_INFO(g_logger) << "main begin -1";
{
sylar::Fiber::GetThis(); // 设置主协程
SYLAR_LOG_INFO(g_logger) << "main begin";
sylar::Fiber::ptr fiber(new sylar::Fiber(run_in_fiber, 0, true)); // 创建一个协程,运行run_in_fiber
fiber->Resume(); // --------------------------> 1
SYLAR_LOG_INFO(g_logger) << "main after swapIn";
fiber->Resume(); // --------------------------> 3
SYLAR_LOG_INFO(g_logger) << "main after end";
fiber->Resume(); // --------------------------> 5
}
SYLAR_LOG_INFO(g_logger) << "main after end2"; // 7
}
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;
}
实现
协程状态
sylar协程一共有5种状态:INIT初始状态、EXEC执行状态、HOLD暂停状态、TERM终止状态、EXCEPT异常状态。他们之间的转换关系如下图所示。

协程刚开始被创建时处于INIT状态,当他执行Resume后,就处于EXEC执行状态;中途可能由于IO事件,会主动让出CPU(通过hook,后面介绍),Yield后,协程处于HOLD状态;当IO事件完成,协程通过Resume恢复运行,此时协程又处于EXEC状态;协程运行完毕,正常退出时,会处于TERM状态;而发生异常时,会进入EXCEPT异常状态。
协程上下文
每个线程拥有两个全局变量,分别保存了当前协程信息和主协程信息。回忆一下,YieldToHold静态方法就是通过t_fiber获取当前协程的信息,然后调用Yield方法切换协程。
static thread_local Fiber *t_fiber = nullptr; // 当前协程
static thread_local Fiber::ptr t_threadFiber = nullptr; // 主协程
协程初始化
值得注意的是每个线程刚开始执行时都需要调用GetThis静态方法设置主协程。
// -------- 主协程 ----------
// GetThis() -> Fiber::Fiber()
Fiber::ptr Fiber::GetThis() {
if (t_fiber) {
return t_fiber->shared_from_this();
}
// 要是t_fiber不存在,则证明主协程还未初始化,因此需要初始化主协程
Fiber::ptr main_fiber(new Fiber);
SYLAR_ASSERT(t_fiber == main_fiber.get());
t_threadFiber = main_fiber; // 设置主协程
return t_fiber->shared_from_this();
}
Fiber::Fiber() {
m_state = EXEC; // 标记协程在运行
SetThis(this); // 设置当前协程为自己
if (getcontext(&m_ctx)) {
SYLAR_ASSERT2(false, "getcontext");
}
++s_fiber_count;
}
// -------- 子协程 -----------
// 只要工作就是为协程分配栈以及设置callback函数
Fiber::Fiber(std::function<void()> cb
,size_t stacksize
,bool not_run_in_scheduler)
: m_id(++s_fiber_id)
,m_cb(cb)
,m_run_in_scheduler(!not_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;
// 设置callback
makecontext(&m_ctx, &Fiber::MainFunc, 0);
}
协程切换
协程切换主要是调用swapcontext进行,里面涉及到调度器协程或主协程和当前协程的切换问题,这将在scheduler节介绍,读者有点印象即可。
void Fiber::Resume() {
SetThis(this);
SYLAR_ASSERT(m_state != EXEC);
m_state = EXEC;
// 若协程被调度器管理,应该和调度器协程切换,scheduler节介绍
if (m_run_in_scheduler) {
if (swapcontext(&Scheduler::GetMainFiber()->m_ctx, &m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
} else {
// 协程没被调度器管理,和主协程切换
if (swapcontext(&t_threadFiber->m_ctx, &m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
}
}
void Fiber::Yield() {
SYLAR_ASSERT(!(m_state == INIT));
if (m_state == EXEC)
m_state = HOLD;
// 若协程被调度器管理,应该和调度器协程切换,scheduler节介绍
if (m_run_in_scheduler) {
SetThis(Scheduler::GetMainFiber());
if (swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
} else {
// 协程没被调度器管理,和主协程切换
SetThis(t_threadFiber.get());
if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
}
}
// 子协程创建时上下文设置为此函数
void Fiber::MainFunc() {
Fiber::ptr cur = GetThis();
SYLAR_ASSERT(cur);
try {
cur->m_cb(); // 然后才真正调用提供的callback
cur->m_cb = nullptr;
cur->m_state = TERM; // 返回代表协程结束,设置TERM状态
} catch (std::exception &ex) {
cur->m_state = EXCEPT;
...
} catch (...) {
cur->m_state = EXCEPT;
...
}
// 为什么要裸指针调用Yield?
auto raw_ptr = cur.get();
cur.reset();
raw_ptr->Yield();
SYLAR_ASSERT2(false,
"never reach fiber_id=" + std::to_string(raw_ptr->getId()));
}
54行提出了一个问题,为什么要用裸指针调用Yield,而不能直接通过cur->Yield()调用?
这是因为协程结束后,将不会再返回,若是一直持有当前协程的智能指针,内存将永远无法被释放,造成内存泄漏。读者可以尝试注释55-60行,然后添加cur->Yield()到函数末尾,然后重新编译运行test_fiber.cc示例,就能看到有的协程没有正常执行析构函数。
其他
reset
reset方法可以在协程退出时,重用协程的栈等资源,实现方式就是替换协程的cb函数,然后重新初始化栈指针,设置上下文为MainFunc函数即可。
这里的关键是协程只有在初始化或者退出的状态时才能reset,否则将造成混乱。
void Fiber::reset(std::function<void()> cb) {
SYLAR_ASSERT(m_stack);
SYLAR_ASSERT(m_state == TERM || m_state == EXCEPT || m_state == INIT);
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;
}