协程调度器
协程调度器定义
协程调度器是用来调配需要完成的协程和函数运行的。对于这一个类最基本的设想是给定一定的线程数,这个类可以自行合理的处理输入需要完成的任务,在所有任务完成后自行停止运行。
完成功能需要解决的问题
作为协程的调度器,必须要运行调度逻辑,那么这个逻辑也应该是在一个协程内进行。这一个调度器的线程内也必须需要标记一个主协程,而线程中也需要对于所属的协程调度器进行标记。这样就引出了两个thread_local变量
static thread_local Scheduler *t_scheduler = nullptr;//标记线程的调度器对象指针,
static thread_local Fiber* t_fiber = nullptr;//标记主协程,此协程内有调度函数(此处需要与协程中自动创建的协程一致)
调度器内部与Fiber内部主协程的区别
Fiber内部的协程仅仅用来标记主协程指针,满足非对称协程需要返回主协程的要求,标记子协程应该返回的地址。而主协程除了转换以外,还在运行线程本身绑定的函数。
调度器需要分为两种
1.其他线程的主协程,主协程的正在运行的函数是调度函数,转换到其他的协程是运行协程里方法。
2.主协程创建完协程调度器对象后该线程需要继续走下去干自己的事或者添加事件,若是使用了stop函数并且user_call为true时主线程和其他线程开始调度加入的函数,否则主线程干自己的事,其他线程将所有任务完成后自动结束
调度器跨线程调度的方法
调度器既然要做多个线程的管理,那么功能也必须是跨线程的。对于这一部分的解决方案是通过包装任务实现的,每个线程都有一个调度主协程,任务中会记录期望执行的线程号。线程在执行任务时检测是否是当前需要的线程号,若指向当前的线程号或-1(指明的任意线程)则运行。
调度器的使用流程
调度器如何来使用也是一个值得讨论的话题,整个操作流程也需要进行精准的分段,加入调度消息的流程和终止的流程未分割好则会导致调度失效。主要流程如下,(user_caller的区别仅在于主线程是否创建了调度协程和停止调度的时候主线程是否进行了一次调度)
如何保证线程调度之前不会提前结束
这个部分主要是指一开始创建对象时是没有任务的,并且start后除主线程的其他线程是执行的run函数。而主线程需要还需要去输入调度消息。这一部分的关键在于空闲(idle)函数的编写,第一次刚开始进入线程时会运行空闲函数知道使用了一次调度以后再次全部进入空闲函数后调度器会自行停止。首先需要声明的是所有线程共享的资源,这些资源是用来控制调度器状态的,空闲函数也是根据这些值来判断
std::vector<Thread::ptr> m_threads;
std::list<FiberAndThread> m_fibers;
Fiber::ptr m_rootFiber;
std::string m_name;
protected:
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_stopping=true;//执行状态
bool m_autoStop=false;
int m_rootThread=0;//主线程id
流程简图
协程调度器结构
结构我主要将写成调度器分成任务部分和调度部分,其中调度部分主要是空闲部分和调度部分,本来还需要注意tickle部分是线程内相互通知的方法,但在调度器内没有进行扩充,空闲部分也只是使用的最简单的实现,调度器需要具体使用可以继承这个类,再对这些函数进行重写
//可能需要重写的三个函数
virtual void tickle();//线程通知
virtual bool stopping();//空闲函数中的停止函数,
//由于使用epoll时可能需要等待事件,这一部分和空闲部分联系较深
virtual void idle();//空闲函数
任务部分
协程调度器调度的是任务,这个任务可以输入一个函数或者一个协程,因此这一部分使用的是一个使用了一个数据结构来作为任务这一个概念的保存。这里的输入任务由两个部分组成,一部分是通过这里的四个构造函数让其可以通过函数或协程进行构造,这一部分是私有的。
struct FiberAndThread {//保存函数或者协程,并让其与线程绑定
Fiber::ptr fiber;
std::function<void()> cb;
int thread;
FiberAndThread(Fiber::ptr f, int thr) : fiber(f), thread(thr) {}
FiberAndThread(Fiber::ptr *f, int thr) : thread(thr) { fiber.swap(*f); }
FiberAndThread(std::function<void()> f, int thr) : cb(f), thread(thr) {}
FiberAndThread(std::function<void()> *f, int thr) : thread(thr) {
cb.swap(*f);
}
FiberAndThread() : thread(-1) {}//方便初始化
void reset() {
fiber = nullptr;
cb = nullptr;
thread = -1;
}
};
添加任务时,下面这一部分使用的是模板函数,作为外部的接口,是模板函数输入前一部分的构造函数进行构造
public:
void schedule(FiberOrCb fc, int thread = -1) {
bool need_tickle = false;
{
MutexType::Lock lock(m_mutex);
need_tickle = scheduleNoLock(fc,thread);
}
if (need_tickle) {
tickle();
}
}
private:
template <class FiberOrCb>
bool scheduleNoLock(FiberOrCb fc, int thread) {
bool need_tickle = m_fibers.empty();
FiberAndThread ft(fc, thread);
if (ft.fiber || ft.cb) {
m_fibers.push_back(ft);
}
return need_tickle;
}
调度部分
对于函数与协程的调度,这一部分若输入的是函数的话需要将函数用协程进行包装。协程不仅可以在运行时候切出,还可以记录切换回来后的状态。调度器也是根据这些状态进行调度
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle,this)));//空闲协程
Fiber::ptr cb_fiber;//可能要运行的函数
{//从消息队列中取出一个需要执行的消息
MutexType::Lock lock(m_mutex);
// SYLAR_LOG_INFO(g_logger) << m_name << "lock";
auto it = m_fibers.begin();
while (it != m_fibers.end()) {//找到是否是线程id
if (it->thread != -1 && it->thread != sylar::GetThreadId()) {//期望的线程id不是当前线程id
++it;
tickle_me = true;//通知其他线程
continue;
}
SYLAR_ASSERT(it->fiber || it->cb);
if (it->fiber && it->fiber->getState() == Fiber::EXEC) {//正在执行什么都不做
++it;
continue;
}
ft = *it;
m_fibers.erase(it++);
++m_activeThreadCount; //此处增加活动线程
is_active=true;
break;
}
///--------函数节选,当ft输入的是函数时
if (ft.cb) {//如果包含的是一个函数的话,那么将它包装程一个协程
if (cb_fiber) {
cb_fiber->reset(ft.cb);
} else {
cb_fiber.reset(new Fiber(ft.cb));
}
ft.reset();//使用过后清空
cb_fiber->swapIn();//此处相当于执行函数
--m_activeThreadCount;
而具体的调度分为两个部分,一个是确定执行的部分
//协程执行部分
ft.fiber->swapIn();//进入要执行的协程
--m_activeThreadCount;//此处已经执行完了,所以活跃线程--
//下面的部分是协程执行后的状态
if (ft.fiber->getState() == Fiber::READY) {//如果是READY状态那么需要再次加入这一个协程
schedule(ft.fiber);
} else if (ft.fiber->getState() != Fiber::TERM &&
ft.fiber->getState() != Fiber::EXCEPT) {//要是函数不会再回去执行了的话,变为HOLD
ft.fiber->m_state=Fiber::HOLD;
}
ft.reset();
}
另一个是当前协程为空的部分,那么就需要进入空闲线程。空闲线程在此处是为了执行stop函数之前让函数一直保持住调度函数的循环。
void Scheduler::idle() {
SYLAR_LOG_INFO(g_logger) << "idle";
while (!stopping()) {
// if(!stopping()){//当调度未开始时,则一直再此循环
sylar::Fiber::YieldToHold(); //若不符合stopping条件,则一直调换回调度函数进行循环
}
}
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle,this)));//空闲协程
//之前绑定了空闲函数
//----------------------------------------------
if (is_active) {
--m_activeThreadCount;
continue;
}
if (idle_fiber->getState() == Fiber::TERM) {//如果空闲协程已经执行过一遍了,那么就退出循环把
SYLAR_LOG_INFO(g_logger)<<"idle fiber term";
break;
}
++m_idleThreadCount;//增加空闲线程
idle_fiber->swapIn();//执行空闲线程
--m_idleThreadCount;
if (idle_fiber->getState() != Fiber::TERM &&
idle_fiber->getState() != Fiber::EXCEPT) {//若回来的状态不等于结束状态时
idle_fiber->m_state = Fiber::HOLD;
}