从零开始实现一个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函数的上下文,导致程序跑飞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/6576.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】表的基本约束

目录 1、约束类型 2、NOT NULL 3、UNIQUE 4、DEFAULT 1、约束类型 关键字解释NOT NULL指示某列不能为空值UNIQUE保证某列的每一行的值不重复DEFAULT当没有给某个列赋值时的默认值PRIMARY KEYNOT NULL 和 UNIQUE 的结合&#xff0c;确保某列(或多列) 有唯一标识&#xff0c…

数据结构_第五关:单链表OJ题练习

OJ题题目简介和链接&#xff1a; 1.删除链表中等于给定值 val 的所有结点。OJ题链接 2.反转一个单链表。OJ题链接 3.给定一个带有头结点 head 的非空单链表&#xff0c;返回链表的中间结点。如果有两个中间结点&#xff0c;则返回第二个中间结点。OJ题链接 4.. 输入一个链表…

如何下载ChatGPT-ChatGPT如何写作

CHATGPT能否改一下文章 ChatGPT 作为一种自然语言处理技术&#xff0c;生成的文章可能存在表达不够准确或文风不符合要求等问题。在这种情况下&#xff0c;可以使用编辑和修改来改变输出的文章&#xff0c;使其符合特定的要求和期望。 具体来说&#xff0c;可以采用以下步骤对…

ChatGPT 从注册到自建应用

这会是关于 ChatGPT 的系列文章&#xff0c;主要记录老胡日常使用 ChatGPT 的一些感想和分享有趣的开源项目&#xff0c;这些信息我都会汇总到一个 ChatGPT 信息群&#xff0c;有兴趣的朋友可以文末加入 &#x1f973;介绍ChatGPT 是由 OpenAI 开发的一种大型自然语言处理模型&…

Linux【环境变量】

文章目录环境变量一、基本概念(1) 环境变量基本介绍(2) 以./作为切入点去了解环境变量(3) 再次认识环境变量二、常见环境变量及相关指令三、通过代码如何获取环境变量四、main函数第三个参数&#xff1a;环境变量参数(1) 第一种方式通过envp获取环境变量(2) 第二种方式通过envi…

Apple Pencil性价比高吗?第三方平替电容笔排名

在现代社会中&#xff0c;电容笔越来越受欢迎&#xff0c;与之相关联的各种功能也在逐步提高。所以&#xff0c;如何选择一款具有高性价比的电容笔就成了人们关注的重点&#xff0c;越来越多人们质量越好越便宜的电容笔。所以&#xff0c;什么牌子的电容笔最便宜&#xff0c;性…

日益强大的人工智能OpenAI ChatGPT GPT-4真的会让程序员失业吗?

今年肯定开始看起来像人工智能起义之年&#xff0c;它进入所有类型的IDE软件只是时间问题。随着微软对OpenAI的至少10亿美元的巨额投资&#xff0c;球真正开始滚动&#xff0c;OpenAI是令人钦佩的强大ChatGPT&#xff0c;Dall-E以及人工智能和机器学习&#xff08;ML&#xff0…

「读书感悟系列」失明症漫记

作者 | gongyouliu编辑 | gongyouliu最近花了不到一周的时间读完了葡萄牙作家萨拉马戈的小说《失明症漫记》&#xff0c;萨拉马戈是葡萄牙到目前为止唯一一位获得诺贝尔文学奖的作家&#xff0c;而这本书就是他的代表作。下面对这本书做一个简单分享。正如书名所说的&#xff0…

蓝桥杯赛前冲刺-双指针和图论专题(包含历年蓝桥杯真题和详细注释代码)

日志统计&#xff08;第九届蓝桥杯省赛CB组,第九届蓝桥杯省赛JAVAB组&#xff09; 小明维护着一个程序员论坛。现在他收集了一份”点赞”日志&#xff0c;日志共有 NN 行。 其中每一行的格式是&#xff1a; ts id 表示在 tsts 时刻编号 idid 的帖子收到一个”赞”。 现在小…

苹果手机配什么无线蓝牙耳机好?适配苹果手机的蓝牙耳机推荐

近年来&#xff0c;TWS耳机越来越受到消费者的欢迎&#xff0c;各家厂商也都推出过不少旗舰级产品&#xff0c;许多产品也是打着苹果平替的旗号来推荐&#xff0c;并且这些耳机在功能和音质上也毫不逊色&#xff0c;并且音质还更加的好&#xff0c;下面整理了几款适用于苹果手机…

网络系统集成实验(三)| 系统集成虚拟局域网(VLAN)配置

目录 一、前言 二、实验目的 三、实验需求 四、实验步骤与现象 Step1&#xff1a;需求分析及配置思路 Step2&#xff1a;实验拓扑设计 Step3&#xff1a;实验配置 Part1&#xff1a;公网IP配置 Part2&#xff1a;链路聚合 Part3&#xff1a;VLAN的创建与划分 Part4&…

abaqus子程序vumat安装使用

一、Win11配置ABAQUS2022VS2022oneAPI2023编译环境 VUMAT是要用到Fortran的&#xff0c;否则添加.for文件会报错 ifort 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。出现这个问题时可以在command中输入命令查看是否有fortran编译器 abaqus infosystem可…

2022蓝桥杯省赛——砍竹子

问题描述 这天, 小明在砍竹子&#xff0c; 他面前有 n 棵竹子排成一排&#xff0c;一开始第 i 棵竹子的 高度为 hi​。 他觉得一棵一棵砍太慢了&#xff0c; 决定使用魔法来砍竹子。魔法可以对连续的一 段相同高度的竹子使用&#xff0c; 假设这一段竹子的高度为 H&#xff0…

【学习记录】大数据课程-学习十一周总结

Hive的安装 Hive的安装方式 hive的安装一共有三种方式:内嵌模式、本地模式、远程模式 元数据服务(metastore&#xff09;作用是&#xff1a;客户端连接metastore服务&#xff0c;metastore再去连接MySQL数据库来存取元数据。有了metastore服务&#xff0c;就可以有多个客户端…

企业数据平台建设的基石:构建统一的数据存算能力

随着企业数字化程度的逐步提高&#xff0c;数字化业务对数据管理的需求也持续深化。根据企业本身所处的数字化程度不同&#xff0c;我们将企业的数据平台的建设总结为五个阶段&#xff0c;本篇我们对统一的数据存储与算力做介绍。 — 整体介绍 — 企业发展的战略目标就是为了更…

约会Appointment

前言 加油 原文 约会常用会话 ❶ The meeting is scheduled for Friday afternoon. 会议安排在星期五下午。 ❷ He got a date with Amanda tomorrow night. 明天晚上他跟阿曼达有个约会。 ❸ They’re going to honeymoon in Europe. 他们打算在欧洲度蜜月。 ❹ Will yo…

考研数二第十讲 求导平面曲线的切线和法线以及曲率圆与曲率半径和弧微分

关于函数的导数几何意义&#xff0c;一元函数和二元函数存在一些不同&#xff0c;二元或多元函数求导叫做对应的偏导数&#xff0c;函数求导以及平面曲线切线&#xff0c;法线求解或者根据已知切线求函数会与其他题型结合考察&#xff0c;单独出题概率比较小。曲率和曲率半径求…

Java Web 实战 15 - 计算机网络之网络编程套接字

文章目录一 . 网络编程中的基本概念1.1 网络编程1.2 客户端(client) / 服务器(server)1.3 请求(request) / 响应(response)1.4 客户端和服务器之间的交互数据1.4.1 一问一答1.4.2 多问一答1.4.3 一问多答1.4.4 多问多答二 . socket 套接字2.1 UDP 的 Socket API2.1.1 引子2.1.2…

通过python理解光的偏振

文章目录基本原理椭圆偏振光基本原理 光是横波&#xff0c;可以写成E⃗A⃗cos(ωt−k⃗r⃗)\vec E \vec{A}cos(\omega t-\vec k\vec r)EAcos(ωt−kr)&#xff0c;振动方向与传播方向垂直&#xff0c;而在三维空间中&#xff0c;与光线垂直的乃是法平面。换言之&#xff0c;光…

jsp+javaEE高校毕业生去向跟踪管理系统gzyy84程序mysql

1&#xff09;登录模块&#xff1a; 管理员的登录&#xff1a;管理员登录系统对本系统其他管理模块进行管理&#xff0c; 以及档案端应用管理员登录后向服务器传输数据。 学生的登陆&#xff1a;学生登陆系统对本系统其他管理模块进行管理。 2&#xf…
最新文章