192
社区成员




这个作业属于哪个课程 | 广工2023软件工程课程 |
---|---|
这个作业要求在哪里 | 作业要求 |
文章内容 | 协程调度模块 |
模块 | 主要负责人 |
---|---|
日志模块 | 钟海超 |
配置模块 | 李昊旃 |
线程模块 | 江周勉 |
协程模块 | 宫旭 |
协程调度模块 | 赵光明 |
I/O协程调度模块 | 李伟东 |
Hook模块 | 邱棋浩(组长) |
时间 | 任务 |
---|---|
今天 | 协程调度模块 |
明天 | IO协程调度模块 |
📸📸重复利用每一个线程📸📸
当你有很多协程时,如何把这些协程都消耗掉,这就是协程调度。
在前面的协程模块中,对于每个协程,都需要用户手动调用协程的resume方法将协程运行起来,然后等协程运行结束并返回,再运行下一个协程。这种运行协程的方式其实是用户自己在挑选协程执行,相当于用户在充当调度器,显然不够灵活.
引入协程调度后,则可以先创建一个协程调度器,然后把这些要调度的协程传递给调度器,由调度器负责把这些协程一个一个消耗掉。
从某种程度来看,协程调度其实非常简单,简单到用下面的代码就可以实现一个调度器,这个调度器可以添加调度任务,运行调度任务,并且还是完全公平调度的,先添加的任务先执行,后添加的任务后执行。
一个 N-M 的协程调度器,N 个线程运行 M 个协程,协程可以在线程之间进行切换,协程也可以绑定到指定线程运行。
实现协程调度之后,可以解决前一章协程模块中子协程不能运行另一个子协程的缺陷,子协程可以通过向调度器添加调度任务的方式来运行另一个子协程。
协程调度器调度的是协程,函数(可执行对象)被包装成协程。
协程调度器类。
t_scheduler_fiber 保存当前线程的调度协程,加上 Fiber 模块的 t_fiber 和 t_thread_fiber,每个线程总共可以记录三个协程的上下文信息。
一个简单的协程调度器实现如下:
Scheduler::Scheduler(size_t threads, bool use_caller, const std::string& name)
:m_name(name) {
SYLAR_ASSERT(threads > 0);
SYLAR_LOG_INFO(g_logger) << "Scheduler() ";
// 复用该线程
if(use_caller) {
// 创建主协程
sylar::Fiber::GetThis();
--threads;
SYLAR_ASSERT(GetThis() == nullptr);
t_scheduler = this;
// 协程运行run方法
m_rootFiber.reset(new Fiber(std::bind(&Scheduler::run, this), 0, true));
sylar::Thread::SetName(m_name);
// 记录当前线程中的执行fiber
t_fiber = m_rootFiber.get();
m_rootThread = sylar::GetThreadId();
// 当前线程加入线程池
m_threadIds.push_back(m_rootThread);
}
else {
m_rootThread = -1;
}
m_threadCount = threads;
}
start方法调用后,创建线程池,线程数量由初始化的线程数和use_caller确定
调度线程一旦创建,将立刻从任务队列中取任务执行
若use_caller为true,则start方法什么也不做(不需要创建新的线程用于调度)
void Scheduler::start() {
MutexType::Lock lock(m_mutex);
if(!m_stopping) {
return ;
}
m_stopping = false;
SYLAR_ASSERT(m_threads.empty());
// 申请给定大小的线程池
m_threads.resize(m_threadCount);
for(size_t i = 0;i<m_threadCount;++i) {
// 每个线程绑定Scheduler::run函数
m_threads[i].reset(new Thread(std::bind(&Scheduler::run, this)
, m_name + "_" + std::to_string(i)));
// 记录线程id
m_threadIds.push_back(m_threads[i]->getId());
}
lock.unlock();
}
其在一个while循环中,不断从任务队列中取任务并且执行。当任务队列为空时,会进入idle协程,进行一个等待,若有新任务,则返回run,若检测到结束,则idle的状态为TERM,随后run方法跳出循环,结束调度。
void Scheduler::run() {
set_hook_enable(true);
setThis();
if(sylar::GetThreadId() != m_rootThread) {
t_fiber = Fiber::GetThis().get();
}
// 空闲协程 后续子类重写Scheduler::idle方法实现epoll_wait阻塞
Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle, this)));
Fiber::ptr cb_fiber;
// 记录协程、线程、回调函数
FiberAndThread ft;
SYLAR_LOG_INFO(g_logger) << "run";
while(true) {
ft.reset();
bool tickle_me = false;
bool is_active = false;
{
MutexType::Lock lock(m_mutex);
auto it = m_fibers.begin();
while(it != m_fibers.end()) {
// 当前线程不是指定执行线程,跳过
if(it->thread != -1 && it->thread != sylar::GetThreadId()) {
++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;
}
}
if(tickle_me) {
tickle();
}
// 取出的是协程任务,使用协程执行
if(ft.fiber && (ft.fiber->getState() != Fiber::TERM
&& ft.fiber->getState() != Fiber::EXCEPT)) {
// 进入任务协程执行,本协程挂起
ft.fiber->swapIn();
--m_activeThreadCount;
// 执行完后状态为READY,该协程继续放入管理器
if(ft.fiber->getState() == Fiber::READY) {
scheduler(ft.fiber);
}
// 执行完后状态不是TERM和EXCEPT,更改状态为HOLD,下次再次执行
else if(ft.fiber->getState() != Fiber::TERM
&& ft.fiber->getState() != Fiber::EXCEPT) {
ft.fiber->m_state = Fiber::HOLD;
}
ft.reset();
}
// 取出的是回调函数,放入协程执行
else 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;
// 重置任务 状态为READY,该协程继续放入管理器
if(cb_fiber->getState() == Fiber::READY) {
scheduler(cb_fiber);
// 复用协程,重置
cb_fiber.reset();
}
// 执行完毕或出错,协程置空
else if(cb_fiber->getState() == Fiber::EXCEPT
|| cb_fiber->getState() == Fiber::TERM) {
cb_fiber->reset(nullptr);
}
// 其他状态时,协程挂起,重置协程,等待下次使用
else{
cb_fiber->m_state = Fiber::HOLD;
cb_fiber.reset();
}
}
// 协程任务,状态为执行超时或出错
else {
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;
}
}
}
}
void Scheduler::idle() {
SYLAR_LOG_INFO(g_logger) << "idle";
while(!stopping()) {
sylar::Fiber::YieldToHold();
}
}
void Scheduler::stop() {
m_autoStop = true;
if(m_rootFiber
&& m_threadCount == 0
&& (m_rootFiber->getState() == Fiber:: TERM
|| m_rootFiber->getState() == Fiber::INIT)) {
SYLAR_LOG_INFO(g_logger) << this << "stopped";
m_stopping = true;
if(stopping()) {
return ;
}
}
if(m_rootThread != -1) {
SYLAR_ASSERT(GetThis() == this);
}
else {
SYLAR_ASSERT(GetThis() != this);
}
m_stopping = true;
for(size_t i=0;i<m_threadCount;++i) {
tickle();
}
if(m_rootFiber) {
tickle();
}
if(m_rootFiber) {
if(!stopping()) {
// 执行主协程
m_rootFiber->call();
}
}
std::vector<Thread::ptr> thrs;
{
MutexType::Lock lock(m_mutex);
thrs.swap(m_threads);
}
// 依次唤醒线程,等待执行完毕
for(auto& x:thrs) {
x->join();
}
}
协程调度最难理解的地方是当 caller 线程也参与调度时调度协程和主线程切换的情况。
在非对称协程里,子协程只能和线程主协程切换,而不能和另一个子协程切换。而这里,调度协程和任务协程,都是子协程,也就是说,调度协程不能直接和任务协程切换。sylar 的解决方案是:给每个线程增加一个线程局部变量用于保存调度协程的上下文就可以了,这样,每个线程可以同时保存三个协程的上下文,一个是当前正在执行的协程上下文,另一个是线程主协程的上下文,最后一个是调度协程的上下文。
调度协程执行 run 方法,负责从调度器的任务队列中取任务执行,取出的任务即子协程。每个子协程执行完后都必须返回调度协程,由调度协程重新从任务队列中取新的协程并执行。如果任务队列空了,那么调度协程会切换到一个 idle 协程,这个 idle 协程什么也不做,等有新任务进来前,不断地与调度协程进行切换(这里其实是忙等)。
调度器的停止行为要分两种情况讨论,首先是 use_caller 为 false 的情况,这种情况下,由于没有使用 caller 线程进行调度,那么只需要简单地等各个调度线程的调度协程退出就行了。如果 use_caller 为 true,表示 caller 线程也参与了调度,这时,调度器初始化时记录的属于 caller 线程的调度协程就要起作用了,在调度器停止前,应该让这个 caller 线程的调度协程也运行一次,让 caller 线程完成调度工作后再退出。如果调度器只使用了 caller 线程进行调度,那么所有的调度任务要在调度器停止时才会被调度(因为只使用了 caller 线程进行调度的话,就意味着用的是 caller 线程的子协程进行调度,而只有在调度器停止时,该子协程才会被 call())。
今天是敏捷冲刺的第五天,内容是实现协程调度模块。是我个人学习C++所接触到的第一个项目,期间遇到了不少困难,好在与团队开发过程中不断地进行交流解惑,才能顺利的完成这一模块。相信在团队成员的相互扶持下,未来所有的难题都将迎难而解。