【源码讲解】sylar服务器框架----协程调度模块

为什么要有协程调度模块?

        一个子协程无法直接执行另外一个子协程,必须切换回线程的主协程,由主协程选出下一个执行的子协程。因此需要协程调度模块,另外一定协程调度模块一定要是多线程并行执行多个子协程的,这样子才能充分利用到多核性能。线程在创建和销毁的时候会有很大的额外开销(相比协程而言),因此内部实现一个线程池,在调用start成员函数的时候创建线程,在调用stop成员函数的后,任务队列没有任务可以执行的时候才会销毁线程。期间若线程没有任务可以执行,就进入idle状态,执行idle线程。

协程调度模块概述:

        重要的成员变量有,存线程池的vector,这个是一个vector容器,里面的成员是之前封装的Thread类。还有一个任务队列,是一个list容器,容器里面的成员是ScheduleTask,还有一个vector容器存全部可以调度协程的线程的线程id的数组(若调度器线程也参与调度,则里面也有调度器线程的id)。还有线程池线程总数,活跃线程数和idle线程数,调度器所在线程的调度协程的指针(初始化后,里面跑的入口函数是run函数)。

/// 协程调度器名称
    std::string m_name;
    /// 互斥锁
    MutexType m_mutex;
    /// 线程池
    std::vector m_threads;
    /// 任务队列
    std::list m_tasks;
    /// 线程池的线程ID数组
    std::vector m_threadIds;
    /// 工作线程数量,不包含use_caller的主线程
    size_t m_threadCount = 0;
    /// 活跃线程数
    std::atomic m_activeThreadCount = {0};
    /// idle线程数
    std::atomic m_idleThreadCount = {0};

    /// 是否use caller
    bool m_useCaller;
    /// use_caller为true时,调度器所在线程的调度协程
    Fiber::ptr m_rootFiber;
    /// use_caller为true时,调度器所在线程的id
    int m_rootThread = 0;

    /// 是否正在停止(是否调用了stop函数)
    bool m_stopping = false;

        调度器调用start函数后,会存在着两种线程。一种是调度器线程,另外一种是调度线程。调度线程用于选出协程执行,调用start函数后被创建,调用stop后被销毁。调度器线程则是调度器所在的线程,负责创建子线程---调度线程以及回收资源。如果构造函数里面传入了true的参数,也需要负责选出协程执行,若传入false就不需要。

任务队列里面的调度任务:

        为了方便,调度任务既可以是协程,也可以是函数,另外还需要指定在哪个线程上调度执行。ScheduleTask类含有三个成员变量,分别是Fiber类,function和thread,thread用于记录指定运行的函数。

        以下类为调度任务类(ScheduleTask)

        

struct ScheduleTask {
        Fiber::ptr fiber;
        std::function 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 f, int thr) {
            cb     = f;
            thread = thr;
        }
        ScheduleTask() { thread = -1; }

        void reset() {
            fiber  = nullptr;
            cb     = nullptr;
            thread = -1;
        }
    };

 协程调度器运行全过程:

构造函数:

        构造函数共传入三个参数,分别是调度器创建线程数,是否将当前线程也作为调度协程,以及调度器线程的名字。对于第二个参数,若传入为true,那么除了调度器线程外,只会创建如n-1个调度线程,因为调度器线程也会参与到协程调度中来。若传入false,那么就会创建出n个线程,这样调度器线程不会参与到协程调度中。调度器线程也参与协程调度的目的是减少线程切换的开销,线程越少,切换次数越少,开销也就越少。

        若调度器线程参与协程调度,首先会调用Fiber的GetThis函数来初始化线程的主协程,然后将要创建的线程数减一,然后将this指针赋值给t_scheduler,然后创建出一个子协程,协程入口函数传入run成员函数,这个子协程不参与协程调度,将其指针赋值给m_rootFiber。这个子协程是调度器线程的调度协程。调度器的主协程不会被调度协程所调度,而且,当调度协程停止的时候,应该返回调度器的主协程,所以要分别存调度器的主协程和调度协程。然后将调度协程的指针赋值给t_scheduler_fiber,然后将线程id赋值给m_rootThread,并放进m_threadIds数组中。

        若调度器线程不参与协程调度,那么m_rootThread直接赋值-1即可。

 start函数:

        start函数用于创建线程池中的线程。若构造函数使用了use_caller参数,则创建n - 1个线程,若设置为false,则创建n个线程。首先上互斥锁,然后判断是否正在停止,若是则返回并报错。然后依次创建线程,线程绑定的入口函数为Scheduler类的run函数,参数为this指针,并Thread的指针放进m_threads中,然后将线程id放入m_threadIds中,start函数执行结束。所有子线程开始执行run函数。

 run函数:

        首先将this指针赋值给t_scheduler,若当前线程的id不是m_rootThread,那么就获取当前线程的主协程,将其赋值给t_scheduler_fiber。然后创建一个idle协程,协程的入口函数绑定为Scheduler的idle函数,接着创建一个cb_fiber协程作为子协程来调度,再创建一个ScheduleTask,用于临时存储任务。然后进入一个无限循环中,在循环中,首先清空task,接着上互斥锁,然后使用迭代器,遍历任务队列中的全部调度务。若遍历到的这个任务指定了调度线程,但是不是当前线程的话,就标记一下需要通知其他线程进行调度,然后跳过这个调度任务。取到调度任务后,首先判断这个ScheduleTask是否有function或Fiber。然后判断调度任务的协程是否处于运行状态。然后从任务队列里删除这个调度任务,线程准备执行子协程,活动线程数加一。若任务队列还有剩余,也需要标记一下需要通知其他线程进行调度。然后执行tickle函数进行通知。然后开始执行子协程,若调度任务中存的是协程,开始调用协程的resume方法进行执行,当子协程中调用yield方法返回到调度协程中来的时候,就说明协程要么执行完了,要么半路yield(半路yield未执行完的函数由hook模块重新加入到任务队列中进行调度,这个以后再说),总之任务就执行结束了,活跃线程数减一,并且清空调度任务。如果调度任务重存的是函数,那么就创建出一个协程,并将指针赋值给cb_fiber,由cb_fiber执行协程,然后进行同上面一样的处理。若都不存在,就说明刚才任务队列遍历到最后都没有找到可以执行的调度任务,也就是说任务队列已经没有任务可以执行了。首先判断idle协程是否处于结束态,是的话就说明调度器已经调用了stop方法,函数返回。若不处于结束态就继续执行,然后idle协程数加一,开始调度idle协程,idle协程若返回则idle协程数减一并且继续执行。

 stop函数:

        stop函数用于停止调度器的运行。当已经调用过一次stopping函数后,并且任务队列为空,并且没有活跃线程数的话,说明已经是正在处于停止的状态中,stop函数终止。接着设置m_stopping为true,这样子idle协程就不会调用yield函数,而是Idle协程结束执行,idle协程进入结束态,在idle协程结束之后,调度线程会结束运行。通知调度线程然后判断调度器线程是否被加入到调度中来,若加入到了,只能由调度器线程调用stop函数。然后调用tickle函数,通知其他全部函数离开idle函数,运行选出协程运行的代码。如果使用了调度器所在的线程进行调度的话,那么就调用m_rootFiber协程的resume函数,开始执行run函数(为什么这里要执行run函数一会再说)。然后调用n次join函数,回收调度线程的资源,函数结束执行。

为什么stop函数中,如果调度器线程也加入到协程调度来,需要调用绑定了run函数的协程?

        当只有调度器线程参与协程调度的时候,不能立即执行调度协程。因为直接执行调度协程,只能进行协程调度,若没有任务,则会进入忙等待的状态,也就是说无法调用stop函数结束协程执行。所以,选择在执行stop函数后,再进行协程调度,运行任务队列里的协程,任务队列被清空后,再回到stop函数继续执行。

idle函数:

        本服务器框架,当任务队列为空的时候,调度协程会执行idle协程。由于设计的问题,假如阻塞在idle协程中,那么当向任务队列中添加协程的时候,怎么检测到任务队列不为空并退出呢?这是无解的,所以只能不断地检测任务队列,cpu处于忙等待状态。所以说协程调度模块没有使用价值........但是在后面的io协程调度模块中解决了这个问题,阻塞在epoll_wait函数中,并且通过检测管道是否写入来退出idle函数。

tickle函数:

        用来通知调度线程退出idle协程,但是本模块无实际作用,只是作为一个接口供派生类重写。

析构函数:

        只有当调度器停止后才能调度用析构函数,否则会报错。然后将t_scheduler置为空。

你可能感兴趣的:(c++,服务器)