scheduler协程调度模块
以下是从sylar服务器中学的,对其的复习;
参考资料
- 实现了一个N-M的协程调度器,N个线程运行M个协程,协程可以在线程之间进行切换,也可以绑定到指定线程运行。
实现协程调度之后,可以解决前一章协程模块中子协程不能运行另一个子协程的缺陷,子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。
既然多线程可以提高协程调度的效率,那么,能不能把调度器所在的线程(称为caller线程)也加入进来作为调度线程呢?比如典型地,在main函数中定义的调度器,能不能把main函数所在的线程也用来执行调度任务呢?答案是肯定的,在实现相同调度能力的情况下(指能够同时调度的协程数量),线程数越小,线程切换的开销也就越小,效率就更高一些,所以,调度器所在的线程,也应该支持用来执行调度任务。甚至,调度器完全可以不创建新的线程,而只使用caller线程来进行协程调度,比如只使用main函数所在的线程来进行协程调度。
调度器内部维护一个任务队列和一个调度线程池。开始调度后,线程池从任务队列里按顺序取任务执行。调度线程可以包含caller线程。当全部任务都执行完了,线程池停止调度,等新的任务进来。添加新任务后,通知线程池有新的任务进来了,线程池重新开始运行调度。停止调度时,各调度线程退出,调度器停止工作。
sylar协程调度模块设计
sylar的协程调度模块支持多线程,支持使用caller线程进行调度,支持添加函数或协程作为调度对象,并且支持将函数或协程绑定到一个具体的线程上执行。
首先是协程调度器的初始化。sylar的协程调度器在初始化时支持传入线程数和一个布尔型的use_caller参数,表示是否使用caller线程。在使用caller线程的情况下,线程数自动减一,并且调度器内部会初始化一个属于caller线程的调度协程并保存起来(比如,在main函数中创建的调度器,如果use_caller为true,那调度器会初始化一个属于main函数线程的调度协程)。
调度器创建好后,即可调用调度器的schedule方法向调度器添加调度任务,但此时调度器并不会立刻执行这些任务,而是将它们保存到内部的一个任务队列中。
接下来是调用start方法启动调度。start方法调用后会创建调度线程池,线程数量由初始化时的线程数和use_caller确定。调度线程一旦创建,就会立刻从任务队列里取任务执行。比较特殊的一点是,如果初始化时指定线程数为1且use_caller为true,那么start方法什么也不做,因为不需要创建新线程用于调度。并且,由于没有创建新的调度线程,那只能由caller线程的调度协程来负责调度协程,而caller线程的调度协程的执行时机与start方法并不在同一个地方。
接下来是调度协程,对应run方法。调度协程负责从调度器的任务队列中取任务执行。取出的任务即子协程,这里调度协程和子协程的切换模型即为前一章介绍的非对称模型,每个子协程执行完后都必须返回调度协程,由调度协程重新从任务队列中取新的协程并执行。如果任务队列空了,那么调度协程会切换到一个idle协程,这个idle协程什么也不做,等有新任务进来时,idle协程才会退出并回到调度协程,重新开始下一轮调度。
在非caller线程里,调度协程就是调度线程的主线程,但在caller线程里,调度协程并不是caller线程的主协程,而是相当于caller线程的子协程,这在协程切换时会有大麻烦(这点是sylar协程调度模块最难理解的地方),如何处理这个问题将在下面的章节专门进行讨论。
接下来是添加调度任务,对应schedule方法,这个方法支持传入协程或函数,并且支持一个线程号参数,表示是否将这个协程或函数绑定到一个具体的线程上执行。如果任务队列为空,那么在添加任务之后,要调用一次tickle方法以通知各调度线程的调度协程有新任务来了。
在执行调度任务时,还可以通过调度器的GetThis()方法获取到当前调度器,再通过schedule方法继续添加新的任务,这就变相实现了在子协程中创建并运行新的子协程的功能。
接下来是调度器的停止。调度器的停止行为要分两种情况讨论,首先是use_caller为false的情况,这种情况下,由于没有使用caller线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果use_caller为true,表示caller线程也要参于调度,这时,调度器初始化时记录的属于caller线程的调度协程就要起作用了,在调度器停止前,应该让这个caller线程的调度协程也运行一次,让caller线程完成调度工作后再退出。如果调度器只使用了caller线程进行调度,那么所有的调度任务要在调度器停止时才会被调度。
模块实现
首先是对协程模块的改造,增加m_runInScheduler成员,表示当前协程是否参与调度器调度,在协程的resume和yield时,根据协程的运行环境确定是和线程主协程进行交换还是和调度协程进行交换
Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler)
: m_id(s_fiber_id++)
, m_cb(cb)
, m_runInScheduler(run_in_scheduler) {
++s_fiber_count;
m_stacksize = stacksize ? stacksize : g_fiber_stack_size->getValue();
m_stack = StackAllocator::Alloc(m_stacksize);
if (getcontext(&m_ctx)) {
SYLAR_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);
SYLAR_LOG_DEBUG(g_logger) << "Fiber::Fiber() id = " << m_id;
}
void Fiber::resume() {
SYLAR_ASSERT(m_state != TERM && m_state != RUNNING);
SetThis(this);
m_state = RUNNING;
// 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
if (m_runInScheduler) {
if (swapcontext(&(Scheduler::GetMainFiber()->m_ctx), &m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
} else {
if (swapcontext(&(t_thread_fiber->m_ctx), &m_ctx)) {
SYLAR_ASSERT2(false, "swapcontext");
}
}
}
void Fiber::yield() {
/// 协程运行完之后会自动yield一次,用于回到主协程,此时状态已为结束状态
SYLAR_ASSERT(m_state == RUNNING || m_state == TERM);
SetThis(t_thread_fiber.get());
if (m_state != TERM) {
m_state = READY;
}
// 如果协程参与调度器调度,那么应该和调度器的主协程进行swap,而不是线程主协程
if (m_runInScheduler) {
if (swapcontext(&m_ctx, &(Scheduler::GetMainFiber()->m_ctx))) {
SYLAR_ASSERT2(false, "swapcontext");
}
} else {
if (swapcontext(&m_ctx, &(t_thread_fiber->m_ctx))) {
SYLAR_ASSERT2(false, "swapcontext");
}
}
}
然后是对调度任务的定义,如下,这里任务类型可以是协程/函数二选一,并且可指定调度线程
/**
* @brief 调度任务,协程/函数二选一,可指定在哪个线程上调度
*/
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;
}
};
接下来是调度器的成员变量,包括以下成员
/// 协程调度器名称
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;
接下来是协程调度模块的全局变量和线程局部变量,这里只有以下两个线程局部变量
/// 当前线程的调度器,同一个调度器下的所有线程指同同一个调度器实例
static thread_local Scheduler *t_scheduler = nullptr;
/// 当前线程的调度协程,每个线程都独有一份,包括caller线程
static thread_local Fiber *t_scheduler_fiber = nullptr;
t_scheduler_fiber保存当前线程的调度协程,加上Fiber模块的t_fiber和t_thread_fiber,每个线程总共可以记录三个协程的上下文信息
调度器的构造方法
/**
* @brief 创建调度器
* @param[in] threads 线程数
* @param[in] use_caller 是否将当前线程也作为调度线程
* @param[in] name 名称
*/
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;
/**
* 在user_caller为true的情况下,初始化caller线程的调度协程
* caller线程的调度协程不会被调度器调度,而且,caller线程的调度协程停止时,应该返回caller线程的主协程
*/
m_rootFiber.reset(new Fiber(std::bind(&Scheduler::run, this), 0, false));
sylar::Thread::SetName(m_name);
t_scheduler_fiber = m_rootFiber.get();
m_rootThread = sylar::GetThreadId();
m_threadIds.push_back(m_rootThread);
} else {
m_rootThread = -1;
}
m_threadCount = threads;
}
Scheduler *Scheduler::GetThis() {
return t_scheduler;
}
两个get方法,用于获取当前线程的调度器的调度协程,这两个都是静态方法
Scheduler *Scheduler::GetThis() {
return t_scheduler;
}
Fiber *Scheduler::GetMainFiber() {
return t_scheduler_fiber;
}
协程调度器的start方法实现,这里主要初始化调度线程池,如果只使用caller线程进行调度,那这个方法啥也不做
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++) {
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());
}
}
判断调度器是否已经停止的方法,只有当所有的任务都被执行完了,调度器才可以停止
bool Scheduler::stopping() {
MutexType::Lock lock(m_mutex);
return m_stopping && m_tasks.empty() && m_activeThreadCount == 0;
}
调度器的tickle和idle实现,目前没什么用
void Scheduler::tickle() {
SYLAR_LOG_DEBUG(g_logger) << "ticlke";
}
void Scheduler::idle() {
SYLAR_LOG_DEBUG(g_logger) << "idle";
while (!stopping()) {
sylar::Fiber::GetThis()->yield();
}
}
调度协程的实现,内部有一个while(true)循环,不停地从任务队列取任务并执行,由于Fiber类改造过,每个被调度器执行的协程在结束时都会回到调度协程,所以这里不用担心跑飞问题,当任务队列为空时,代码会进idle协程,但idle协程啥也不做直接就yield了,状态还是READY状态,所以这里其实就是个忙等待,CPU占用率爆炸,只有当调度器检测到停止标志时,idle协程才会真正结束,调度协程也会检测到idle协程状态为TERM,并且随之退出整个调度协程。这里还可以看出一点,对于一个任务协程,只要其从resume中返回了,那不管它的状态是TERM还是READY,调度器都不会自动将其再次加入调度,一个成熟的协程是要学会自我管理的
void Scheduler::run() {
SYLAR_LOG_DEBUG(g_logger) << "run";
setThis();
if (sylar::GetThreadId() != m_rootThread) {
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();
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);
}
// 当前调度线程找到一个任务,准备开始调度,将其从任务队列中剔除,活动线程数加1
task = *it;
m_tasks.erase(it++);
++m_activeThreadCount;
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";
}
调度器的stop方法,在使用了caller线程的情况下,调度器依赖stop方法来执行caller线程的调度协程,如果调度器只使用了caller线程来调度,那调度器真正开始执行调度的位置就是这个stop方法
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) {
tickle();
}
/// 在use caller情况下,调度器协程结束时,应该返回caller协程
if (m_rootFiber) {
m_rootFiber->resume();
SYLAR_LOG_DEBUG(g_logger) << "m_rootFiber end";
}
std::vector<Thread::ptr> thrs;
{
MutexType::Lock lock(m_mutex);
thrs.swap(m_threads);
}
for (auto &i : thrs) {
i->join();
}
}
注意事项
sylar的协程调度模块因为存任务队列空闲时调度线程忙等待的问题,所以实际上并不实用,真正实用的是后面基于Scheduler实现的IOManager。由于任务队列的任务是按顺序执行的,如果有一个任务占用了比较长时间,那其他任务的执行会受到影响,如果任务执行的是像while(1)这样的循环,那线程数不够时,后面的任务都不会得到执行。另外,当前还没有实现hook功能,像sleep和等待IO就绪这样的操作也会阻塞协程调度。