Scheduler.h
#ifndef _SCHEDULER_H_
#define _SCHEDULER_H_
#include "thread.h"
#include "fiber.h"
#include <mutex>
#include <vector>
using namespace yjx
{
class Scheduler
{
public:
//构造函数:构造Scheduler对象(线程池中线程的数量,user_caller指定是否将主线程作为工作协程,name调度器的名称也就是标识符)
Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "Scheduler");
virtual ~Scheduler();// 虚析构函数,需要派生类重写此函数,保证派生类对象能够正确调用
// 如果不是virtual 那么派生类中定义任何清理资源的函数都不会被执行,可能导致资源泄露,派生类对象不完全销毁的问题
const std::string& getName() { return m_name ; } // 获取调度器的名字
public:
static Scheduler* GetThis(); // 获取调度器对象
protected:
// 只有类的实例才能使用该方法
// 设置正在运行的调度器
void SetThis();
public:
// 设置任务队列
// FiberOrCb : 调度任务类型可以是对象或函数指针.
// 模板类:允许类或方法接受任意类型的参数,
template <class FiberOrCb> // 避免代码冗余
// 线程和线程的任务对应俩参数
void schedulerlock(FiberOrCb fc, int thread = -1) // 这里fc就可以是任意参数,可能是函数,可能是协程
{
bool need_tickle; // 需要tickle,就是标记任务队列是否为空,从而判断是否需要唤醒线程
{
std::lock_guard<std::mutex> lock(m_mutex); // 不允许手动加锁解锁,只有等待程序结束自己析构
// 如果为空,说明所有的线程都处于不活跃的状态idle,需要被唤醒
need_tickle = m_tasks.empty();
// 创建任务队列对象
SchedulerTask task(fc, thread);
if (task.fiber || task.cb) // 任务可能是函数可能是协程
{
m_tasks.push_back(task);
}
// 判断队列是否为空,不为空通知线程开始调度
if (need_tickle) tickle();
}
}
virtual void start(); // 启动调度器
virtual void stop(); // 停止调度器
// 调度过程
protected:
virtual void tickle(); // 唤醒线程
virtual void run(); // 执行任务
virtual void idle(); // 空闲协程,无任务时执行idle
virtual bool stopping(); // 是否可以关闭 用于stop
// 是否有空闲线程
// 当调度协程进入idle(任务执行完了或者等待执行下一个任务,说明线程空闲了)空闲+1, 从idle协程返回(开始执行)空闲线程-1
bool hasIdleThreads() {return m_idleThreadCount>0;}
// 任务:可能是协程也可能是函数,所以上面定义了一个模板类,避免冗余代码
private:
struct SchedulerTask
{
std::shared_ptr<Fiber> fiber;
std::function<void()> cb;
int thread; // 运行指定任务的线程id
SchedulerTask() // 初始化协程,入口函数,运行协程的线程id
{
fiber = nullptr;
cb = nullptr;
thread = -1;
}
// 任务是协程
SchedulerTask(std::shared_ptr<Fiber> f, int thr) {
fiber = f;
thread = thr;
}
SchedulerTask(std::shared_ptr<Fiber>* f, int thr) {
fiber.swap(*f); // 将内容转移指针内部的转移,引用计数不会增加,相当于把自己的给别人,自己-1,别人+1,别管谁引用,计数1次
thread = thr;
}
// 任务是函数
SchedulerTask(std::function<void()> cb, int thr) {
cb = f;
thread = thr;
}
SchedulerTask(std::shared_ptr<Fiber>* f, int thr) {
cb.swap(*f);
thread = thr;
}
void reset() // 重置 每一个任务执行完重置状态等待下一个任务,也是释放执行权
{
fiber = nullptr;
cb = nullptr;
thread = -1;
}
};
private:
// 调度器标识符
std::string m_name;
// 互斥锁:用来保护任务队列, 因为只有主线程能访问调度器
std::mutex m_mutex;
// 线程池:初始化好的线程
std::vector<std::shared_ptr<Thread>> m_threads;
// 任务队列
std::vector<SchdeulerTask> m_tasks;
// 存储工作线程id
std::vector<int> m_ThreadIds;
// 需要额外创建的线程数
size_t m_threadCount = 0;
// 活跃的线程数
std::atomic<size_t> m_activeThreadCount = {0};
// 空闲的线程数
std::atomic<size_t> m_idleThreadCount = {0};
// 主线程是否用于工作线程
bool m_userCaller;
// 如果是-》需要创建额外的调度协程
std::shared_ptr<Fiber> m_schedulerFiber;
// 如果是-》记录主线程的id
int m_rootThreadId = -1;
// 调度器是否关闭
bool m_stopping = false;
};
}
#endif
1. 主线程自身参与调度器调度的情况(user_caller == true),主线程在空闲状态也去执行任务,减少cpu资源浪费,主线程只有在stop()的时候才开始工作
主线程创建主协程负责执行main(),创建调度协程负责调度器。刚创建主协程时会把调度协程默认设置为主协程,此时主线程调用reset()再创建一个协程并通过方法Fiber::SetSchedulerFiber(Fiber* f) 将其设置为调度协程,该调度协程绑定了run
std::shared_ptr<Fiber> scheduler = make_shared<Scheduler>(n, true, name_);
主协程创建线程池,负责执行任务,该方法会根据n创建含有n-1一个工作线程的线程池,因为主线程也参与调度,即也能作为工作协程去工作,所以是n-1
scheduler->start()
然后开始给任务队列添加任务(到此时控制权都还在主协程手里,工作线程也没有被唤醒)
scheduler->schedulerLock(task)
最后主协程->stop(),主线程的控制权给他自己的调度协程去创建子协程工作,
scheduler->stop()
在这里首先会确认控制权,即当前运行的协程,然后将所有的线程都唤醒,避免挂起,导致永久阻塞在等待唤醒的状态,然后确认调度协程,唤醒并resume,此时调度协程才拿到控制权,调度器开始工作,取任务并调度工作线程执行任务,通过互斥锁来实现同步,然后主线程等待所有工作线程执行完毕回收资源join(), 继续执行main(),程序结束。
这里要补充一下,只要启动了线程池,工作线程就开始轮询任务队列,每个线程自己调度自己,主线程也充当工作线程的时机(参与工作的时机)就是主线程要关闭调度器,把控制权给到调度协程也去参与工作即 工作线程一创建就开始轮询, 主线程最后加入清理
2. 主线程不参与调度,只负责创建调度器,额外创建一个线程来进行调度工作
具体流程如下图所示,关于tickle()唤醒操作则是在io+scheduler中实现,目前只要工作线程被创建了就会处于一个忙等待状态,自身的主协程和空闲协程会一直来回切换,直到任务到来执行任务具体工作流程如下图