sylar-from-scratch----协程调度模块scheduler

51 阅读11分钟

图片1.png

协程调度:当注册表中有多个协程时,如何将这些协程消耗掉,称为协程调度。
协程调度器:创建一个协程调度器,将要调度的协程传递给调度器,由调度器负责将各协程消耗掉。(不同于手动调度) 线程池中的每个线程都有一个主协程,所有线程的调度器只有一个?
调度器所在线程是否存在于线程池中?
调度器是如何作用于不同线程的协程的?
线程中主协程如何决定下一个运行的协程?

协程调度模块

封装一个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;
        }
    };