协程模块概述
概念
首先介绍一下为什么要使用协程。从了解进程,线程,协程之间的区别开始。
-
从定义来看
- 进程是资源分配和拥有的基本单位。进程通过内存映射拥有独立的代码和数据空间,若没有内存映射给进程独立的空间,则没有进程的概念了。
- 线程是程序执行的基本单位。线程都处在一个进程空间中,可以相互访问,没有限制,所以使用线程进行多任务变成十分便利,所以当一个线程崩溃,其他任何一个线程都不能幸免。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。
- 协程是用户态的轻量级线程,线程内部调度的基本单位。协程在线程上执行。
-
从系统调用来看
- 进程由操作系统进行切换,会在用户态与内核态之间来回切换。在切换进程时需要切换虚拟内存空间,切换页表,切换内核栈以及硬件上下文等,开销非常大。
- 线程由操作系统进行切换,会在用户态与内核态之间来回切换。在切换线程时需要保存和设置少量寄存器内容,开销很小。
- 协程由用户进行切换,并不会陷入内核态。先将寄存器上下文和栈保存,等切换回来的时候再进行恢复,上下文的切换非常快
-
从并发性来看
- 不同进程之间切换实现并发,各自占有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_threadFiber
的ucontext_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;
}