从零开始实现一个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多线程模式的服务器。

协程调度模块

  • 协程模块中,我们只能处理主协程与子协程间的切换,无法实现在子协程中调用调用协程,在协程调度模块中可以解决这个问题。子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。
  • 创建一个协程调度器,然后把要调度的协程传递给调度器,由调度器负责把这些协程一个一个消耗掉。这里给出了一个协程调度器的简单实现版本
/**
 * @brief 简单协程调度类,支持添加调度任务以及运行调度任务
 */
class Scheduler {
public:
    /**
     * @brief 添加协程调度任务
     */
    void schedule(sylar::Fiber::ptr task) {
        m_tasks.push_back(task);
    }
 
    /**
     * @brief 执行调度任务
     */
    void run() {
        Fiber::ptr task;
        auto it = m_tasks.begin();
 
        while(it != m_tasks.end()) {
            task = *it;
            m_tasks.erase(it++);
            task->call();
        }
    }
private:
    /// 任务队列
    std::list<sylar::Fiber::ptr> m_tasks;
};

// 协程任务
void test_fiber(int i) {
    std::cout << "hello world " << i << std::endl;
}
 
int main() {
    /// 初始化当前线程的主协程
    Fiber::GetThis();
 
    /// 创建调度器
    Scheduler sc;
 
    /// 添加调度任务
    for(auto i = 0; i < 10; i++) {
        Fiber::ptr fiber(new Fiber(
            std::bind(test_fiber, i)
        ));
        sc.schedule(fiber);
    }
 
    /// 执行调度任务
    sc.run();
 
    return 0;
}

1. 主要功能

  • 实现了一个线程池,支持多线程,多协程协同调度
  • 能够支持将主线程作为调度线程,提高程序运行效率
  • 支持将函数或协程绑定到一个具体的线程上执行

2. 功能演示

void task2(){
	// 任务2
}

void task1() {
	// 任务1
	Scheduler::GetThis().schedule(task2);	// 任务协程中也能添加任务
}

int main(){
	// 创建协程调度器,并使用主线程作为调度线程
	Scheduler sc(1, true, "name");
	sc.schedule(task1);
	sc.start();
	sc.stop();
	
	return 0;
}

3. 模块介绍

3.1 Scheduler

  • 协程调度器,包含了线程池,协程任务队列,调度协程等
class Scheduler
{
public:
	typedef std::shared_ptr<Scheduler> ptr;
	typedef Mutex MutexType;

public:
	Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "");
	virtual ~Scheduler();
	
	void schedule(FiberOrCb fc, int thread = -1);			//添加任务协程
	void schedule(InputIterator begin, InputIterator end);
public:
		
		static Scheduler* GetThis();			// 获取当前协程调度器
		static void SetThis(Scheduler* s);		// 设置当前协程调度器
		static Fiber* GetScheduleFiber();		// 获取当调度协程
private:
		struct FiberAndCallBack{ ... };
protected:	
	void run();					// 协程调度函数
	virtual void idle();		// 协程无任务可调度时执行idle协程
	virtual bool stopping();	// 返回是否结束
	virtual void tickle();		// 唤醒协程

private:
	MutexType m_mutex;						//锁
	std::vector<Thread::ptr> m_threads;		//线程池
	std::list<FiberAndCallBack> m_fibers;	//即将执行和计划执行的协程,由协程完成具体任务
	Fiber::ptr m_schedulFiber;				//调度协程,只在use_caller = true有效
	std::string m_name;						//调度器名称

protected:
	std::vector<int> m_threadIds;			//协程下的线程id数组
	int m_rootThread = 0;					//主线程id(use_caller = true才使用)
	size_t m_threadCount = 0;				//线程池中线程数量
	bool m_stopping = true;					//是否正在停止
	bool m_autoStop = false;				//是否自动停止
	 
	std::atomic<size_t> m_activeThreadCount = { 0 };	//工作线程数量
	std::atomic<size_t> m_idleThreadCount = { 0 };		//空闲线程数量
};

3.2 任务协程

  • 对于协程调度器来说,协程可以是调度任务,但实际上,函数也可以是,只需要把函数包装成协程即可。因此这里设计了一个调度任务FiberAndCallBack
struct FiberAndCallBack
{
	Fiber::ptr fiber;			// 任务协程
	std::function<void()> cb;	// 函数
	int thread;					// 线程id
	
	FiberAndCallBack(Fiber::ptr f, int thr)
		:fiber(f),
		thread(thr)
	{}

	FiberAndCallBack(Fiber::ptr* f, int thr)
		:thread(thr)
	{
		fiber.swap(*f);
	}

	FiberAndCallBack(std::function<void()> f , int thr)
		:cb(f),
		thread(thr)
	{}

	FiberAndCallBack(std::function<void()>* f, int thr)
		:thread(thr)
	{
		cb.swap(*f);
	}

	FiberAndCallBack()
		:thread(-1)
	{}

	void reset()
	{
		fiber = nullptr;
		cb = nullptr;
		thread = -1;
	}
};

3.3 线程池与调度线程

  • 在协程调度模块,我们说单线程同时刻只能运行一个协程。这样效率明显不高。调度器需要用多线程来提高效率,这样就能让多个协程同时执行。
vector<Thread::ptr> m_threads;		//线程池

3.4 让主线程也充当调度线程

  • 为了提高效率,可以选择将主线程也充当调度协程。比如,在main函数中定义了调度器,可以把main函数所在线程也用来执行调度任务。使用use_caller来控制。
// use_caller: true 使用主线程; false 不使用主线程
Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "");

use_caller为true时,创建的协程应当在主协程 和 子协程(调度协程)间交换(call、back)。当use_caller为false时,调度协程 和 调度协程的子协程交换(swapIn、swapOut)。后文还会详细说明。

call/back:		专门负责调度协程和主协程间转换
swapIn/swapOut:专门负责调度协程和任务协程间转换
         call          swapIn
主协程<--->调度协程<--->任务协程
    back      swapOut

3.5 添加任务协程

  • 当在协程中需要添加任务协程时,可以执行调度器的schedule方法
void task() {
	// 任务协程的逻辑处理
}

Fiber::ptr fiber(task)
Scheduler sc;
sc.schedule(fiber);
  • 每次添加完都需要执行tickle方法,告诉其他调度线程有新的任务到来

3.6 如何调度任务

  • 所有调度线程按顺序从任务协程队列中取任务执行,当没有任务时,执行idle协程,等待新的任务到来
// 创建线程池,每个调度线程都注册了一个调度器的执行函数
for (size_t i = 0; i < m_threadCount; ++i)
{
	m_threads[i].reset(new Thread(std::bind(&Scheduler::run, this), m_name + "_" + std::to_string(i)));
	m_threadIds.push_back(m_threads[i]->getId());//这里需要注意,当new Thread的时候,我们wait了一下,等线程执行函数初始化完再notify,这样就保证了能拿到此处的线程id;
}
  • 调度协程依次查看任务协程队列有无任务
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle, this)));//闲置协程
Fiber::ptr cb_fiber;//使用回调函数的协程

while(true) {
	auto it = m_fibers.begin();
	while (it != m_fibers.end())
	{
		//当前协程任务(FiberAndThread)中设置的线程id != 当前线程id(我们指定了每个协程任务(FiberAndThread)应该在哪里跑)
		if (it->thread != -1 && it->thread != johnsonli::getThreadId())
		{
			++it;
			tickle_me = true;
			continue;
		}

		DO_ASSERT(it->fiber || it->cb);
		//协程任务(FiberAndThread)中的协程目前正在EXEC状态,不执行
		if (it->fiber && it->fiber->getState() == Fiber::EXEC) {
			++it;
			continue;
		}

		//找到了一个可以执行的协程任务(FiberAndThread)
		fc = *it;
		m_fibers.erase(it++);//获取当前协程任务(FiberAndThread)后,从协程队列中移除
		++m_activeThreadCount;//增加一个线程执行
		is_active = true;//当前线程存活
		break;
	}

	tickle_me |= (it != m_fibers.end());//如果还没有遍历完,就可以通知调度器有任务
	if (tickle_me) { tickle(); //通知调度器有任务 }
	if (fc.fiber && (fc.fiber->getState() != Fiber::TERM
				&& fc.fiber->getState() != Fiber::EXCEPT)) {
		fc.fiber->swapIn(); //执行任务协程
		....
	}else if (fc.cb) { //协程任务有回调函数
		cb_fiber.reset(new Fiber(fc.cb));
		fc.reset();
		cb_fiber->swapIn();	//执行任务协程
	}
	else {
		idle.swapIn(); //执行idle协程
	}
	
}
  • idle协程需要轮询等待,新的任务协程到来
void Scheduler::idle() { 

	//如果没结束,就切换成HOLD(暂停)状态,这样就不会退出当前线程
	//这里要使用while,因为下一次swapIn,还会再判断一次
	while(!stopping()) {
		Fiber::YieldToHoldBySwap();
	}
}

3.7 调度器停止

  • 调度器应该支持停止调度的功能,以便回收调度线程的资源,只有当所有的调度线程都结束后,调度器才算真正停止
void Scheduler::stop() {
	m_autoStop = true;

	//调度协程!=null && 调度协程状态=TERM(已经完成) | INIT(还未开始) && 当前线程池为null
	if (m_schedulFiber
		&& (m_schedulFiber->getState() == Fiber::TERM || m_schedulFiber->getState() == Fiber::EXCEPT || m_schedulFiber->getState() == Fiber::INIT)
		&& m_threadCount == 0) {
		LOG_INFO(g_logger) << this << " stopped";
		m_stopping = true;

		if (stopping()) return;
	}

	//use_caller,使用主线程时,只在主线程中stop
	if (m_rootThread != -1) //m_scheduleFiber
	{
		DO_ASSERT(GetThis() == this);
	}
	else
	{
		//不使用主线程,任意线程都可以stop
		DO_ASSERT(GetThis() != this);
	}

	m_stopping = true;

	//唤醒线程池中的线程
	for (size_t i = 0; i < m_threadCount; ++i) {
		tickle();
	}

	//唤醒调度协程
	if (m_schedulFiber) tickle();

	//调度协程 != null 有m_scheduleFiber 一定是在主线程
	if (m_schedulFiber) {
		if (!stopping()) m_schedulFiber->call();
	}

	std::vector<Thread::ptr> thrs;
	{
		MutexType::Lock lock(m_mutex);
		thrs.swap(m_threads);
	}

	//让其他线程先结束,留一个线程来回收
	//如果在主线程stop,可能主线程先结束,然后sc就释放,但是子线程还在跑,使用sc,会core dump
	for (auto& i : thrs) {
		i->join();
	}
}

4. 调度器执行流程

  • 协程调度器初始化。协程调度器在初始化时支持传入线程数和一个布尔型的use_caller参数,表示是否使用主线程作为调度线程。在使用主线程的情况下,线程数自动减一,并且调度器内部会初始化一个属于主线程的调度协程并保存起来(比如,在main函数中创建的调度器,如果use_caller为true,那调度器会初始化一个属于main函数线程的调度协程)。
  • 调度器创建好后,即可调用调度器的schedule方法向调度器添加调度任务,但此时调度器并不会立刻执行这些任务,而是将它们保存到内部的一个任务协程队列中。
  • 调用start方法启动调度。start方法调用后会创建调度线程池,线程数量由初始化时的线程数和use_caller确定。调度线程一旦创建,就会立刻从任务队列里取任务执行。比较特殊的一点是,如果初始化时指定线程数为1且use_caller为true,那么start方法什么也不做,因为不需要创建新线程用于调度。并且,由于没有创建新的调度线程,那只能由主线程的调度协程来负责调度协程,而主线程的调度协程的执行时机与start方法并不在同一个地方,它只在stop中执行。
  • 进入调度协程的run方法。调度协程负责从调度器的协程任务队列中取任务执行。取出的任务即子协程,这里调度协程和子协程的切换模型即为前一章介绍的非对称模型,每个子协程执行完后都必须返回调度协程,由调度协程重新从协程任务队列中取新的任务并执行。如果任务队列空了,那么调度协程会切换到一个idle协程,这个idle协程什么也不做,等有新任务进来时,idle协程才会退出并回到调度协程,重新开始下一轮调度。
    • 这里需要注意:在非主线程里,调度协程就是调度线程的主协程,但在主线程里,调度协程并不是主线程的主协程,而是相当于主线程的子协程。
  • 在执行调度任务时,还可以通过调度器的GetThis()方法获取到当前调度器,再通过schedule方法继续添加新的任务,这就变相实现了在子协程中创建并运行新的子协程的功能。
  • 调度器停止。调度器的停止行为要分两种情况讨论,首先是use_caller为false的情况,这种情况下,由于没有使用caller线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果use_caller为true,表示caller线程也要参于调度,这时,调度器初始化时记录的属于caller线程的调度协程就要起作用了,在调度器停止前,应该让这个caller线程的调度协程也运行一次,让caller线程完成调度工作后再退出。如果调度器只使用了caller线程进行调度,那么所有的调度任务要在调度器停止时才会被调度。

5. 调度协程切换问题

  • 情况1:线程数为1,use_caller为false,应额外创建一个线程进行协程调度、main函数线程不参与调度的情况。
    • 因为有单独的线程用于协程调度,那只需要让新线程的入口函数作为调度协程,从协程任务队列里取任务执行就行了,main函数与调度协程完全不相关,main函数只需要向调度器添加任务,然后在适当的时机停止调度器即可。当调度器停止时,main函数要等待调度线程结束后再退出。
      从零开始实现一个C++高性能服务器框架----协程调度模块_第1张图片
  • 情况2:线程数为1,且use_caller为true,对应只使用main函数线程进行协程调度的情况。
    • 当只有main函数线程调度任务时,会存在以下三类协程:
      • main函数对应的主协程
      • 调度协程
      • 待调度的任务协程
    • 这三类协程运行的顺序如下:
      • main函数主协程运行,创建调度器
      • 仍然是main函数主协程运行,向调度器添加一些调度任务
      • 开始协程调度,main函数主协程让出执行权,切换到调度协程,调度协程从任务队列里按顺序执行所有的任务
      • 每次执行一个任务,调度协程都要让出执行权,再切到该任务的协程里去执行,任务执行结束后,还要再切回调度协程,继续下一个任务的调度
      • 所有任务都执行完后,调度协程还要让出执行权并切回main函数主协程,以保证程序能顺利结束。
        从零开始实现一个C++高性能服务器框架----协程调度模块_第2张图片
    • 抽象以下,就可以得到以下协程模型
      从零开始实现一个C++高性能服务器框架----协程调度模块_第3张图片
      非对称协程里,子协程只能和线程主协程切换,而不能和另一个子协程切换。在上面的情况1中,线程主协程是main函数对应的协程,另外的两类协程,也就是调度协程和任务协程,都是子协程,也就是说,调度协程不能直接和任务协程切换,一旦切换,程序的main函数协程就跑飞了。
    • 这里程序跑飞的关键是,线程只有两个线程局部变量保存主协程和子协程的上下文信息。也就是说线程任何时候都最多只能知道两个协程的上下文。如果子协程和子协程切换,那这两个上下文都会变成子协程的上下文,线程主协程的上下文丢失了,程序也就跑飞了。
    • 因此,需要给每个线程增加一个线程局部变量用于保存调度协程的上下文就可以了,这样,每个线程可以同时保存三个协程的上下文,一个是当前正在执行的协程上下文,另一个是线程主协程的上下文,最后一个是调度协程的上下文。有了这三个上下文,协程就可以根据自己的身份来选择和每次和哪个协程进行交换。
    call/back:		专门负责调度协程和主协程间转换
    swapIn/swapOut:专门负责调度协程和任务协程间转换
             call          swapIn
    主协程<--->调度协程<--->任务协程
        back      swapOut
    

从零开始实现一个C++高性能服务器框架----协程调度模块_第4张图片

你可能感兴趣的:(服务器,c++)