从零开始实现一个C++高性能服务器框架----协程模块
此项目是根据sylar框架实现,是从零开始重写sylar,也是对sylar丰富与完善
项目地址:https://gitee.com/lzhiqiang1999/server-framework
简介
项目介绍:实现了一个基于协程的服务器框架,支持多线程、多协程协同调度;支持以异步处理的方式提高服务器性能;封装了网络相关的模块,包括socket、http、servlet等,支持快速搭建HTTP服务器或WebSokcet服务器。
详细内容:日志模块,使用宏实现流式输出,支持同步日志与异步日志、自定义日志格式、日志级别、多日志分离等功能。线程模块,封装pthread相关方法,封装常用的锁包括(信号量,读写锁,自旋锁等)。IO协程调度模块,基于ucontext_t实现非对称协程模型,以线程池的方式实现多线程,多协程协同调度,同时依赖epoll实现了事件监听机制。定时器模块,使用最小堆管理定时器,配合IO协程调度模块可以完成基于协程的定时任务调度。hook模块,将同步的系统调用封装成异步操作(accept, recv, send等),配合IO协程调度能够极大的提升服务器性能。Http模块,封装了sokcet常用方法,支持http协议解析,客户端实现连接池发送请求,服务器端实现servlet模式处理客户端请求,支持单Reator多线程,多Reator多线程模式的服务器。
协程模块
- 每个协程在创建时都会指定一个入口函数,类似线程。协程的本质就是函数和函数运行状态的组合。
- 在普通函数中,函数一旦被调用,只能从头开始执行,直到函数执行结束退出;协程可以执行一半就退出(call),但并未真正结束,只是暂时让出CPU执行权,之后可以恢复运行(back)。在暂停运行期间,其他协程可以获得CPU并运行,因此协程也称为轻量级线程。
- 本协程模块时基于ucontext_t实现,也就是协程上下文,包含了函数在当前执行状态下的全部CPU寄存器的值(函数栈帧,代码执行位置等),具体信息如下所示
// 上下文结构体定义
// 这个结构体是平台相关的,因为不同平台的寄存器不一样
// 下面列出的是所有平台都至少会包含的4个成员
typedef struct ucontext_t {
// 当前上下文结束后,下一个激活的上下文对象的指针,只在当前上下文是由makecontext创建时有效
struct ucontext_t *uc_link;
// 当前上下文的信号屏蔽掩码
sigset_t uc_sigmask;
// 当前上下文使用的栈内存空间,只在当前上下文是由makecontext创建时有效
stack_t uc_stack;
// 平台相关的上下文具体内容,包含寄存器的值
mcontext_t uc_mcontext;
...
} 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是本模块非对称协程实现的关键,线程主协程和子协程用这个接口进行上下文切换
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
- 关于协程,详细讲解可以参考如何简单通俗的理解协程
1. 主要功能
- 使用非对称协程模型,简化程序逻辑
- 由用户控制协程的执行逻辑,实现了主协程与子协程间的自由切换
- 每个线程有一个主协程t_threadFiber,由主协程创建子协程,通过call()进入子协程运行,back()退出子协程,返回主协程
2. 功能演示
Logger::ptr logger = LOG_ROOT();
void run_in_fiber() {
LOG_INFO(logger) << "run in fiber";
}
int main() {
LOG_INFO(logger) << "main begin";
Fiber::Getthis();
Fiber::ptr fiber(new Fiber(run_fiber))
fiber.call();
LOG_INFO(logger) << "main end";
return 0;
}
3. 模块介绍
3.1 Fiber
- 协程模块,根据协程状态,实现主协程和子协程间的相互切换
- 协程状态
enum State
{
INIT , // 初始状态
READY, // 可执行状态
RUNNING, // 运行状态
TERM, // 结束状态
};
- 协程主要功能
class Fiber : public std::enable_shared_from_this<Fiber>
{
public:
typedef std::shared_ptr<Fiber> ptr;
public:
/// <summary>
/// 无参构造函数
/// 每个线程第一个协程的构造,主协程
/// </summary>
Fiber();
public:
/// <summary>
/// 构造函数
/// </summary>
/// <param name="cb">协程执行函数</param>
/// <param name="stacksize">协程栈大小</param>
/// <param name="use_caller">Schedule中是否使用主线程</param>
Fiber(std::function<void()> cb, size_t stacksize = 0, bool use_caller = false);
~Fiber();
//重置协程函数,并重置状态
//pre: getState() 为 INIT, TERM, EXCEPT
//post: INIT
void reset(std::function<void()> cb);
// 将当前协程切换到运行状态, Scheduler调度线程 --> 当前线程
void swapIn();
// 退出当前协程,当前协程 --> Scheduler调度协程
void swapOut();
// 将当前协程切换到执行状态 主协程-->当前协程
void call();
// 将当前协程切换到后台,当前协程-->主协程
void back();
public:
/// <summary>
/// 设置当前线程的运行协程
/// </summary>
/// <param name="fiber">运行协程</param>
static void SetThis(Fiber* fiber);
/// <summary>
/// 返回当前协程
/// </summary>
/// <returns>一个全局静态变量 static thread_globle Thread* t_fiber</returns>
static Fiber::ptr GetThis();
// 协程切换到后台,并设置为READY状态。回到Scheduler协程
static void YieldToReadyBySwap();
// 协程切换到后台,并设置为HOLD状态。回到Scheduler协程
static void YieldToHoldBySwap();
// 协程切换到后台,并设置为Ready状态。回到主协程
static void YieldToReadyByBack();
// 协程切换到后台,并设置为HOLD状态。回到主协程
static void YieldToHoldByBack();
/// <summary>
/// 协程的工作函数,执行完成返回到Scheduler协程
/// </summary>
static void MainFunc();
/**
* @brief 协程执行函数
* @post use_caller时有效,Scheduler会使用主线程的时候
*/
static void CallerMainFunc();
private:
uint64_t m_id = 0; //协程id
uint32_t m_stacksize = 0; //协程运行栈大小
State m_state = INIT; //协程状态
ucontext_t m_ctx; //协程上下文
void* m_stack = nullptr; //协程运行栈指针
std::function<void()> m_cb; //协程工作函数
};
- 主协程。没有协程执行函数
Fiber::Fiber() {
m_state = EXEC; //主协程创建好后,是运行中状态
SetThis(this); //设置当前协程 t_fiber
if (getcontext(&m_ctx)) { DO_ASSERT2(false, "getcontext"); }
++s_fiber_count;
m_id = ++s_fiber_id;
LOG_DEBUG(g_logger) << "Fiber::Fiber main id=" + std::to_string(GetId());
}
- 子协程。需要设置协程执行函数
Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool use_caller)
:m_id(++s_fiber_id)
,m_cb(cb)
{
++s_fiber_count;
m_stacksize = stacksize ? stacksize : g_fiber_stack_size->getValue();
m_stack = StackAllocator::Alloc(m_stacksize); // 分配协议栈
if (getcontext(&m_ctx)) { DO_ASSERT2(false, "getcontext"); }
m_ctx.uc_link = nullptr;
m_ctx.uc_stack.ss_sp = m_stack;
m_ctx.uc_stack.ss_size = m_stacksize;
// Scheduler调度协程中使用主线程参与调度
if (use_caller)
{
makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
}
else
{
makecontext(&m_ctx, &Fiber::MainFunc, 0);
}
}
- 主协程和子协程间的相互切换
// 主协程-->子协程
void Fiber::call() {
// 设置当前执行的协程:主协程-->字协程
SetThis(this);
m_state = EXEC;
if (swapcontext(&t_threadFiber->m_ctx, &m_ctx)) { DO_ASSERT2(false, "swapcontext"); }
}
// 子协程-->主协程
void Fiber::back() {
// 设置当前执行的协程:子协程-->主协程
SetThis(t_threadFiber.get());
if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) { DO_ASSERT2(false, "swapcontext"); }
}
//协程切换到后台,并设置为READY状态
//回到主协程
void Fiber::YieldToReadyByBack() {
Fiber::ptr cur = GetThis();
cur->m_state = READY;
cur->back();
}
//协程切换到后台,并设置为HOLD状态
//回到主协程
void Fiber::YieldToHoldByBack() {
Fiber::ptr cur = GetThis();
cur->m_state = HOLD;
cur->back();
}
- 配合Schedule调度协程模块,为了提高效率,通过
use_caller
控制是否把主线程加入到调度协程任务上,因此涉及到调度协程和子协程间的切换,主协程和子协程间的切换
Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool use_caller)
{
//....
// Scheduler调度协程中使用主线程参与调度
if (use_caller)
{
// 在主协程和主协程间切换
makecontext(&m_ctx, &Fiber::CallerMainFunc, 0);
}
else
{
// 在调度协程和子协程间切换
makecontext(&m_ctx, &Fiber::MainFunc, 0);
}
}
void Fiber::swapIn() // 调度协程-->当前协程
void Fiber::swapOut() // 当前协程-->调度协程
- 协程入口函数
/**
* @brief 协程入口函数
* @note 这里没有处理协程函数出现异常的情况,同样是为了简化状态管理,并且个人认为协程的异常不应该由框架处理,应该由开发者自行处理
*/
void Fiber::CallerMainFunc() {
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->back(); // 协程结束时自动back,以回到主协程
}
- 协程重置。重置协程就是重复利用已结束的协程,复用其栈空间,创建新协程,实现如下
// 只有TERM状态的协程才可以重置
void Fiber::reset(std::function<void()> cb) {
DO_ASSERT(m_stack);
DO_ASSERT(m_state == TERM || m_state == INIT);
m_cb = cb;
if (getcontext(&m_ctx)) {
DO_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;
}
3.2 线程局部变量实现协程模块
- 线程局部变量与全局变量类似,不同之处在于,线程局部变量的每个线程都独有一份;全部变量是全部线程共享一份。
static thread_local
- 使用线程局部变量保存协程上下文,每个线程都要独自管理协程,不同线程的协程相互不影响
//当前运行协程
static thread_local Fiber* t_fiber = nullptr;
//主协程
static thread_local Fiber::ptr t_threadFiber = nullptr;
t_fiber
是当前运行的协程,t_threadFiber
是主协程,通过setThis()
设置当前运行的协程,getThis()
获取当前的协程
// 没有当前协程时,会创建一个主协程
Fiber::ptr Fiber::GetThis()
void Fiber::SetThis(Fiber* fiber)
使用swapcontext
来做协程切换,意味着,这两个线程局部变量必须至少有一个是用来保存线程主协程的上下文,如果这两个线程局部变量存储的都是子协程的上下文,那么不管怎么调用swapcontext
,都没法恢复主协程的上下文,也就意味着程序最终无法回到主协程去执行,程序也就跑飞了。
// 当前协程-->主协程
void Fiber::back()
{
//当前协程-->主协程
SetThis(t_threadFiber.get());
if (swapcontext(&m_ctx, &t_threadFiber->m_ctx)) {
DO_ASSERT2(false, "swapcontext");
}
}
3.3 注意
- 子协程不能直接call另一个子协程,像下面这样的代码会直接让程序跑飞:
Logger::ptr g_logger = LOG_ROOT();
void run_in_fiber2() {
LOG_INFO(g_logger) << "run_in_fiber2 begin";
LOG_INFO(g_logger) << "run_in_fiber2 end";
}
void run_in_fiber() {
LOG_INFO(g_logger) << "run_in_fiber begin";
/**
* 非对称协程,子协程不能创建并运行新的子协程,下面的操作是有问题的,
* 子协程再创建子协程,原来的主协程就跑飞了
*/
Fiber::ptr fiber(new Fiber(run_in_fiber2));
fiber->call();
LOG_INFO(g_logger) << "run_in_fiber end";
}
int main(int argc, char *argv[]) {
LOG_INFO(g_logger) << "main begin";
// 创建主协程,t_threadFiber被设置为当前协程,t_fiber也设置为当前协程
Fiber::GetThis();
// 创建子协程
Fiber::ptr fiber(new Fiber(run_in_fiber));
// 运行子协程,t_fiber设置为子协程
fiber->call();
LOG_INFO(g_logger) << "main end";
return 0;
}
究其原因,在于上面的run_in_fiber本身是一个子协程,在其内部执行另一个协程的call时,swapcontext
会把run_in_fiber的上下文保存到t_threadFiber中,导致t_threadFiber不再指向main函数的上下文,导致程序跑飞。