协程就是用户线程,由用户调用,操作系统无法感知到用户线程,用户可以完全控制调度器。
对于协程的介绍,请观看一下视频及文章,本文不再对协程的概念进行详细讲解。
【协程第一话】协程到底是怎样的存在?_哔哩哔哩_bilibili
【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili
【协程革命】理论篇!扫盲,辟谣一条龙!全语言通用,夯实基础,准备起飞! 全程字幕_哔哩哔哩_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后,协程执行中的状态。
结束态:协程执行完毕后,等待回收协程资源的状态。
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<void()> 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<void()> 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 <string>
#include <vector>
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<sylar::Thread::ptr> 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;
}