协程调度:当注册表中有多个协程时,如何将这些协程消耗掉,称为协程调度。
协程调度器:创建一个协程调度器,将要调度的协程传递给调度器,由调度器负责将各协程消耗掉。(不同于手动调度)
线程池中的每个线程都有一个主协程,所有线程的调度器只有一个?
调度器所在线程是否存在于线程池中?
调度器是如何作用于不同线程的协程的?
线程中主协程如何决定下一个运行的协程?
协程调度模块
封装一个N-M的协程调度器,其内部有一个线程池,支持协程在线程池中切换。一个线程同一时刻只能运行一个协程,协程调度器使用多线程来提高调度的效率,即同一时刻由多个协程可以同时执行。
caller协程:调度器所在的线程;
在实现相同调度能力的情况下(指能够同时调度的协程数量),线程数越小,线程切换的开销也就越小,效率就更高;故而调度器所在的线程也应支持用来执行调度任务,来减少一个额外的线程。
sylar调度模块支持多线程,支持使用caller线程进行调度,支持添加函数或协程作为调度对象,并且支持将函数或协程绑定到具体的线程上运行。
参数
/// 协程调度器名称
std::string m_name;
/// 互斥锁
MutexType m_mutex;
/// 线程池
std::vector<Thread::ptr> m_threads;
/// 任务队列
std::list<ScheduleTask> m_tasks;
/// 线程池的线程ID数组
std::vector<int> m_threadIds;
/// 工作线程数量,不包含use_caller的主线程
size_t m_threadCount = 0;
/// 活跃线程数:正在执行调度任务的线程数
std::atomic<size_t> m_activeThreadCount = {0};
/// idle线程数:不在执行调度任务的线程数
std::atomic<size_t> m_idleThreadCount = {0};
/// 是否use caller
bool m_useCaller;
/// use_caller为true时,调度器所在线程的调度协程
Fiber::ptr m_rootFiber;
/// use_caller为true时,调度器所在线程的id
int m_rootThread = 0;
/// 该调度器是否正在停止
bool m_stopping = false;
类中静态变量
当前线程调度器:不同线程间的协程可以实现切换,这些线程的调度器为同一个调度器。
调度协程:use_caller为true时,调度协程不为线程的主协程,而是线程中的其中一个子协程。 use_caller为false时,调度协程为线程的主协程。
/// 当前线程的调度器,同一个调度器下的所有线程共享同一个实例
static thread_local Scheduler *t_scheduler = nullptr;
/// 当前线程的调度协程,每个线程都独有一份
static thread_local Fiber *t_scheduler_fiber = nullptr;
构造函数---协程调度器初始化
创建调度器,初始化协程数size_t threads为1,默认将当前线程作为调度器,调度器名称为“Schedulaer”。
Scheduler(size_t threads = 1, bool use_caller = true, const std::string &name = "Scheduler");
在使用当前线程作为caller线程时,线程数自动减一(不用创建调度线程,caller线程的子协程所谓调度器,caller线程记为调度线程),即少创建一个线程执行任务,效率更高,并且初始化一个属于caller线程的调度协程并保存起来(如在main函数中创建的调度器。若use_caller为true时,那调度器会初始化一个属于main函数线程的调度协程)。
Scheduler::Scheduler(size_t threads, bool use_caller, const std::string &name) {
SYLAR_ASSERT(threads > 0);
m_useCaller = use_caller;
m_name = name;
if (use_caller) {//将协程调度线程纳入调度器
--threads;
sylar::Fiber::GetThis();//调度线程的主协程
SYLAR_ASSERT(GetThis() == nullptr);
t_scheduler = this;//设置当前协程调度器
/**
* caller线程的主协程不会被线程的调度协程run进行调度,而且,线程的调度协程停止时,应该返回caller线程的主协程
* 在user caller情况下,把caller线程的主协程暂时保存起来,等调度协程结束时,再resume caller协程
*/
m_rootFiber.reset(new Fiber(std::bind(&Scheduler::run, this), 0, false));
sylar::Thread::SetName(m_name);
t_scheduler_fiber = m_rootFiber.get();//caller线程主协程
m_rootThread = sylar::GetThreadId();//线程id
m_threadIds.push_back(m_rootThread);//将调度器线程存入线程池
} else {//当use_caller为false,调度线程id为-1
m_rootThread = -1;
}
m_threadCount = threads;//线程池中线程个数
}
添加调度任务schedule()
模板函数,参数为被调度的对象FiberOrCb(可以是协程对象也可以是函数指针),指定该协程运行的线程thread(-1表示任意线程),将协程或函数绑定到具体的线程上执行。向调度器中添加调度任务,保存在队列m_tasks中,若添加前发现任务队列为空,则在添加任务后,调用tickle通知各调度线程的调度协程有新任务来了。
template <class FiberOrCb>
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(); // 通知线程
}
}
//添加调度任务(无锁)
template <class FiberOrCb>
bool scheduleNoLock(FiberOrCb fc, int thread) {
bool need_tickle = m_tasks.empty();//判断协程任务队列是否为空
ScheduleTask task(fc, thread);
if (task.fiber || task.cb) {//若协程对象或指针至少有一个存在
m_tasks.push_back(task);//将其放入任务队列
}
return need_tickle;
}
启动调度器start()
根据线程数创建线程池m_threadCount,调度线程一旦创建就会从任务队列中取任务执行。若初始化时指定线程数为1且当前线程为调度线程时(use_caller=true),start函数则什么都不做,因为不需要创建新的线程用于调度,此时只使用caller线程的调度协程来完成协程的调度任务,而caller协程的执行时机与start不在同一个地方。
void Scheduler::start() {
SYLAR_LOG_DEBUG(g_logger) << "start";
MutexType::Lock lock(m_mutex);
if (m_stopping) {//调度器已停止调度,输出日志信息并立即返回
SYLAR_LOG_ERROR(g_logger) << "Scheduler is stopped";
return;
}
SYLAR_ASSERT(m_threads.empty());
m_threads.resize(m_threadCount);//根据线程数重置线程池大小
for (size_t i = 0; i < m_threadCount; i++) {//线程执行run任务
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());
}
}
协程调度run()
调度协程负责从调度器的任务队列中取任务执行。取出的任务为子协程,调度协程与子协程以非对称方式进行切换,每个子协程执行完后必须返回调度协程,由调度协程重新从任务队列中取出新的协程并执行。若任务队列为空了,则调度协程会切换到一个idle协程,idle协程不做任何操作,等有新任务进来时idle协程才会退出并回到调度协程,重新开始下一轮调度。
在非caller线程中,调度协程就是调度线程的主协程,但在caller线程中,调度协程并不是caller线程的主协程,而是相当于caller线程的子协程。
在协程调度时可以通过调度器的GetThis()方法获得当前调度器,再使用schedule方法继续添加新的任务,变相实现了在子协程中创建并运行新的子协程的功能。
存在问题:当任务队列为空时,代码进入idle协程,idle将协程yield,子协程状态变为READY状态,所以就是忙等待,CPU占用率爆炸,只有调度器检测到停止标志时,idle协程才会结束,调度协程也会检测到idle协程状态为TERM,并随之退出整个协程。
void Scheduler::run() {
SYLAR_LOG_DEBUG(g_logger) << "run";
set_hook_enable(true);//启用hook
setThis();//设置当前调度器
if (sylar::GetThreadId() != m_rootThread) {//若当前线程不为use_caller
t_scheduler_fiber = sylar::Fiber::GetThis().get();//线程主协程为调度协程
}
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle, this)));//空闲协程
Fiber::ptr cb_fiber;//回调协程
ScheduleTask task;//调度任务
while (true) {
task.reset();//重置协程、回调函数为空指针,线程为-1
bool tickle_me = false; // 是否tickle其他线程进行任务调度
{
MutexType::Lock lock(m_mutex);
auto it = m_tasks.begin();
// 遍历所有调度任务
while (it != m_tasks.end()) {
if (it->thread != -1 && it->thread != sylar::GetThreadId()) {
// 当前任务指定的线程不是当前线程,标记一下需要通知其他线程进行调度,然后跳过这个任务,继续下一个
++it;
tickle_me = true;
continue;
}
// 找到一个未指定线程,或是指定了当前线程的任务
SYLAR_ASSERT(it->fiber || it->cb);
// if (it->fiber) {
// // 任务队列时的协程一定是READY状态,谁会把RUNNING或TERM状态的协程加入调度呢?
// SYLAR_ASSERT(it->fiber->getState() == Fiber::READY);
// }
// [BUG FIX]: hook IO相关的系统调用时,在检测到IO未就绪的情况下,会先添加对应的读写事件,再yield当前协程,等IO就绪后再resume当前协程
// 多线程高并发情境下,有可能发生刚添加事件就被触发的情况,如果此时当前协程还未来得及yield,则这里就有可能出现协程状态仍为RUNNING的情况
// 这里简单地跳过这种情况,以损失一点性能为代价,否则整个协程框架都要大改
if(it->fiber && it->fiber->getState() == Fiber::RUNNING) {
++it;
continue;
}
// 当前调度线程找到一个任务,准备开始调度,将其从任务队列中剔除,活动线程数加1
task = *it;//取出任务
m_tasks.erase(it++);//任务队列移除当前任务
++m_activeThreadCount;//正在执行的任务数量加1
break;
}
// 当前线程拿完一个任务后,发现任务队列还有剩余,那么tickle一下其他线程
tickle_me |= (it != m_tasks.end());
}
if (tickle_me) {
tickle();
}
if (task.fiber) {
// resume协程,resume返回时,协程要么执行完了,要么半路yield了,总之这个任务就算完成了,活跃线程数减一
task.fiber->resume();
--m_activeThreadCount;
task.reset();
} else if (task.cb) {
if (cb_fiber) {
cb_fiber->reset(task.cb);
} else {
cb_fiber.reset(new Fiber(task.cb));
}
task.reset();
cb_fiber->resume();
--m_activeThreadCount;
cb_fiber.reset();
} else {
// 进到这个分支情况一定是任务队列空了,调度idle协程即可
if (idle_fiber->getState() == Fiber::TERM) {
// 如果调度器没有调度任务,那么idle协程会不停地resume/yield,不会结束,如果idle协程结束了,那一定是调度器停止了
SYLAR_LOG_DEBUG(g_logger) << "idle fiber term";
break;
}
++m_idleThreadCount;
idle_fiber->resume();
--m_idleThreadCount;
}
}
SYLAR_LOG_DEBUG(g_logger) << "Scheduler::run() exit";
}
无任务调度时执行idle()
无任务调度时添加日志信息并在调度器为非停止调度时将当前协程yield,下CPU,主协程上CPU
void Scheduler::idle() {
SYLAR_LOG_DEBUG(g_logger) << "idle";
while (!stopping()) {
sylar::Fiber::GetThis()->yield();
}
}
返回是否可以停止stopping()
当调度器已停止调度且任务队列为空,当前活跃线程数为0时停止调度。
bool Scheduler::stopping() {
MutexType::Lock lock(m_mutex);
return m_stopping && m_tasks.empty() && m_activeThreadCount == 0;
}
停止调度器stop()
分两种情况,其一是use_caller为false时表明没有caller线程进行调度,只需要简单地等待各个调度线程的调度协程退出(join)。其二是use_caller为true表明caller线程也参与调度,此时在调度器初始化时记录的属于caller线程的调度协程就要起作用,在调度停止前,应该让caller线程的调度协程也运行一次,让caller线程完成调度工作再退出。若调度器只使用了caller线程进行调度,则所有调度任务要在调度器停止时才被调度。 停止调度器逻辑:通知各线程“tickle”,唤醒调度协程、等待线程结束;为什么要先交换指针再等待线程结束?
void Scheduler::stop() {
SYLAR_LOG_DEBUG(g_logger) << "stop";
if (stopping()) {//调度器已停止调度
return;
}
m_stopping = true;
/// 如果use caller,那只能由caller线程发起stop
if (m_useCaller) {
SYLAR_ASSERT(GetThis() == this);
} else {
SYLAR_ASSERT(GetThis() != this);
}
for (size_t i = 0; i < m_threadCount; i++) {
tickle();//通知线程池中所有线程的调度器
}
if (m_rootFiber) {//因为use_caller的线程不在线程池中,所以需要单独操作
tickle();
}
/// 在use caller情况下,调度主协程结束时,应该返回caller协程
if (m_rootFiber) {
m_rootFiber->resume();
SYLAR_LOG_DEBUG(g_logger) << "m_rootFiber end";
}
std::vector<Thread::ptr> thrs;//互斥锁将线程交换到thrs中
{
MutexType::Lock lock(m_mutex);
thrs.swap(m_threads);
}
for (auto &i : thrs) {
i->join();//等待线程结束
}
}
析构函数
Scheduler::~Scheduler() {
SYLAR_LOG_DEBUG(g_logger) << "Scheduler::~Scheduler()";
SYLAR_ASSERT(m_stopping);//调度器停止调度
if (GetThis() == this) {//当前线程调度器指针设为空指针
t_scheduler = nullptr;
}
}
协程调度任务的结构体
多种构造方式,所使用参数为协程或者函数二选一,可以指定在哪个线程上调度。函数参数为协程fiber、回调函数cd、线程thread。
struct ScheduleTask {//调度任务的结构体,表示调度执行的任务
Fiber::ptr fiber;//协程
std::function<void()> cb;//回调函数
int thread;//线程
ScheduleTask(Fiber::ptr f, int thr) {
fiber = f;
thread = thr;
}
ScheduleTask(Fiber::ptr *f, int thr) {
fiber.swap(*f);
thread = thr;
}
ScheduleTask(std::function<void()> f, int thr) {
cb = f;
thread = thr;
}
ScheduleTask() { thread = -1; }
void reset() {
fiber = nullptr;
cb = nullptr;
thread = -1;
}
};