【C++大型项目之高性能服务器框架 (四) 】协程调度器&定时器&IO协程管理器篇 (中)

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架

系列上期内容:【C++项目之高性能服务器框架 (三) 】协程调度器&定时器&IO协程管理器篇 (上)

系列下期内容:暂无


目录

前言:

协程调度器

一、整体架构图

二、线程局部变量

[三、Scheduler 类详解](#三、Scheduler 类详解)

[3.1 定义(scheduler.h)](#3.1 定义(scheduler.h))

[3.2 成员变量表](#3.2 成员变量表)

[3.3 构造函数(scheduler.cc)](#3.3 构造函数(scheduler.cc))

[3.4 析构函数(scheduler.cc)](#3.4 析构函数(scheduler.cc))

[3.5 GetThis() / GetMainFiber()(scheduler.cc)](#3.5 GetThis() / GetMainFiber()(scheduler.cc))

[3.6 start() ------ 启动调度器(scheduler.cc)](#3.6 start() —— 启动调度器(scheduler.cc))

[3.7 stop() ------ 停止调度器(scheduler.cc)](#3.7 stop() —— 停止调度器(scheduler.cc))

[3.8 schedule() ------ 提交任务(scheduler.h)](#3.8 schedule() —— 提交任务(scheduler.h))

[3.9 scheduleNoLock() ------ 无锁提交(scheduler.h)](#3.9 scheduleNoLock() —— 无锁提交(scheduler.h))

[3.10 run() ------ 核心调度循环(scheduler.cc)](#3.10 run() —— 核心调度循环(scheduler.cc))

第一部分:从任务队列取任务

第二部分A:执行的是协程

第二部分B:执行的是回调函数

[第四部分:空闲处理(idle 协程)](#第四部分:空闲处理(idle 协程))

[3.11 tickle() / stopping() / idle()](#3.11 tickle() / stopping() / idle())

tickle()(scheduler.cc)

stopping()(scheduler.cc)

idle()(scheduler.cc)

[3.12 switchTo() ------ 协程迁移(scheduler.cc)](#3.12 switchTo() —— 协程迁移(scheduler.cc))

[四、FiberAndThread 结构体详解](#四、FiberAndThread 结构体详解)

[4.1 定义(scheduler.h)](#4.1 定义(scheduler.h))

[五、SchedulerSwitcher 类详解](#五、SchedulerSwitcher 类详解)

[5.1 定义(scheduler.h)](#5.1 定义(scheduler.h))

[5.2 实现(scheduler.cc)](#5.2 实现(scheduler.cc))

[六、批量调度 schedule(begin, end)](#六、批量调度 schedule(begin, end))

结语

---⭐️封面自取⭐️---



前言:

那么上一篇博客我拿以我自己的一些思考从而引出的小故事带着大家了解了我们为什么要有协程调度器定时器IO协程管理器这三个东西,想必有了一个相当不错的印象,那么我们这节课就该讨论这三个东西该如何设计了,也就是代码环节,但考虑到sylar的协程调度器相关内容相当复杂难懂,我们就专门抽出来一篇博客来仔细说下协程调度器,话不多说我们开始。

协程调度器

那么在正式开始前,我们先写一个多线程版的简易调度器来看看一开始不加协程的调度器是什么样的,方便大家去理解我们加上协程后的调度器。

cpp 复制代码
#include <chrono>
#include <iostream>
#include <pthread.h>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include "sylar/log.h"
#include "sylar/thread.h"
#include "sylar/mutex.h"
#include "sylar/macro.h"

int num = 0;
uint64_t loop_times = 100000000;

void sing(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout << sylar::Thread::GetName() << " sing~ " << num << std::endl;
}

void dance(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " dance~ " << num << std::endl;   
}

void rap(){
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
    }
    std::cout <<sylar::Thread::GetName() << " rap~ " << num << std::endl;   
}

class Schedule;


static thread_local Schedule* t_schedule = nullptr;

class Schedule {
public:
    typedef sylar::Mutex MutexType;

    Schedule(size_t size = 1)
        :m_poll_size(size){
        SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");
        setThis();
        m_callerThread = sylar::Thread::GetThis();
    }
    
    ~Schedule(){
        if(GetThis()==this){
            t_schedule = nullptr;
        }
    }

    static Schedule* GetThis(){
        return t_schedule;
    }

    void setThis(){
        t_schedule = this;
    }
    

    void run(){
        MutexType::Lock lock(m_mutex);
        std::function<void()> task;
        auto it = m_tasks_queue.begin();
        while(it != m_tasks_queue.end()){
            task = *it;
            m_tasks_queue.erase(it);
            break;
        }
        if(task != nullptr){
            task();
        }
    }

    void start(){
        std::cout << "Schedule start..." << std::endl;
        std::cout << "Init a thread poll" << std::endl;
        m_thread_poll.resize(m_poll_size);
        std::cout << "Schedule run..." << std::endl;
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
        }
    }
    
    void stop(){
        while(!m_tasks_queue.empty()){
            for(size_t i = 0; i < m_poll_size; ++i){
                m_thread_poll[i].reset(new sylar::Thread(std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
            }
        }
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i]->join();
        }


        std::cout << "Schedule stop..." << std::endl; 
    }
    
    void schedule(std::function<void()> task){
        std::cout << "Push task into queue " << &task << std::endl;
        m_tasks_queue.push_back(task);
    }

private:
    MutexType m_mutex;
    size_t m_poll_size = 0;
    std::vector<sylar::Thread::ptr> m_thread_poll;
    std::list<std::function<void()> > m_tasks_queue;      
    sylar::Thread* m_callerThread = 0;
};


//第二个调度线程
void subThread(){
    std::cout << "Sub Schedule Thread" << std::endl;
    Schedule sc(1);
    sc.schedule(std::bind(&rap));
    sc.start();
    sc.stop();
}



int main(){
    std::cout << "====Schedule====" << std::endl;
    std::cout << "====Main start====" << std::endl;
    
    {
        auto start = std::chrono::high_resolution_clock::now();
       
        Schedule sc(2);
        sc.schedule(std::bind(&subThread));
        sc.schedule(std::bind(&dance));
       // sc.schedule(std::bind(&rap));
        sc.start();
        sc.stop();


        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;

    }

    std::cout << "====Main end====" << std::endl;

    return 0;
}

可以看到我们现在多线程下的调度器的场景:一个调度器线程和多个其他的线程和一系列的函数任务。我们用线程局部变量来控制一个线程只存在一个调度器,调度器可以在任何一个线程中使用,互不影响。

但不难发现,我们这种不加协程的写法有一种劣势:

也就是线程从队列取出函数,直接调用 task()直到这个函数全部执行完、返回,线程才能去取下一个任务 。如果 sing() 要执行 1亿次循环,其他任务(如 dance())只能干等着。**任务之间无法"让出"CPU,**不存在第三种可能:"函数执行到一半,先暂停,我去干点别的,一会儿再回来继续"。

而我们协程(Fiber)本质上就是:

一个可以主动让出(Yield)、之后又能恢复(Resume)继续执行的函数。

我们再来看看简易版协程调度器是什么样的:

cpp 复制代码
#include <chrono>
#include <iostream>
#include <vector>
#include <list>
#include <unistd.h>
#include <functional>
#include <atomic>

#include "sylar/sylar.h"  // 包含 fiber.h / thread.h / mutex.h / log.h / scheduler.h

int num = 0;
uint64_t loop_times = 100000000;

// ============================================================
// 业务代码:看起来几乎没变,但内部可以 Yield 了
// ============================================================

void sing(){
    std::cout << sylar::Thread::GetName() << " [Sing] 开始演唱,fiber=" 
              << sylar::Fiber::GetFiberId() << std::endl;
    
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
        // 【核心改造】每隔 2000w 次主动让出 CPU,让其他协程有机会执行
        if(i % 20000000 == 0 && i != 0){
            std::cout << sylar::Thread::GetName() 
                      << " [Sing] 唱到第 " << i 
                      << " 句,让出麦克风~ fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
            sylar::Fiber::YieldToReady();  // 让出!状态变 READY,重新入队
            std::cout << sylar::Thread::GetName() 
                      << " [Sing] 重新拿回麦克风,继续唱!fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
        }
    }
    std::cout << sylar::Thread::GetName() << " [Sing] 演唱结束~ " << num 
              << " fiber=" << sylar::Fiber::GetFiberId() << std::endl;
}

void dance(){
    std::cout << sylar::Thread::GetName() << " [Dance] 开始跳舞,fiber=" 
              << sylar::Fiber::GetFiberId() << std::endl;
    
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
        if(i % 20000000 == 0 && i != 0){
            std::cout << sylar::Thread::GetName() 
                      << " [Dance] 跳到第 " << i 
                      << " 拍,让出舞台~ fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
            sylar::Fiber::YieldToReady();
            std::cout << sylar::Thread::GetName() 
                      << " [Dance] 重新拿回舞台,继续跳!fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
        }
    }
    std::cout << sylar::Thread::GetName() << " [Dance] 舞蹈结束~ " << num 
              << " fiber=" << sylar::Fiber::GetFiberId() << std::endl;
}

void rap(){
    std::cout << sylar::Thread::GetName() << " [Rap] 开始说唱,fiber=" 
              << sylar::Fiber::GetFiberId() << std::endl;
    
    for(size_t i = 0; i < loop_times; ++i){
        ++num;
        if(i % 20000000 == 0 && i != 0){
            std::cout << sylar::Thread::GetName() 
                      << " [Rap] 念到第 " << i 
                      << " 句,歇口气~ fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
            sylar::Fiber::YieldToReady();
            std::cout << sylar::Thread::GetName() 
                      << " [Rap] 喘好了,继续念!fiber=" 
                      << sylar::Fiber::GetFiberId() << std::endl;
        }
    }
    std::cout << sylar::Thread::GetName() << " [Rap] 说唱结束~ " << num 
              << " fiber=" << sylar::Fiber::GetFiberId() << std::endl;
}

// ============================================================
// 协程版简易调度器
// ============================================================

static thread_local Schedule* t_schedule = nullptr;

class Schedule {
public:
    typedef sylar::Mutex MutexType;

    Schedule(size_t size = 1)
        :m_poll_size(size){
        SYLAR_ASSERT2(GetThis()==nullptr,"one thread one schedule");
        setThis();
        m_callerThread = sylar::Thread::GetThis();
    }
    
    ~Schedule(){
        if(GetThis()==this){
            t_schedule = nullptr;
        }
    }

    static Schedule* GetThis(){ return t_schedule; }
    void setThis(){ t_schedule = this; }

    // 【改造1】run() 不再直接调用函数,而是 swapIn 协程
    void run(){
        // 每个调度线程都要先初始化自己的主协程
        sylar::Fiber::GetThis();

        while(true){
            sylar::Fiber::ptr task;
            bool has_more = false;
            
            {
                MutexType::Lock lock(m_mutex);
                auto it = m_tasks_queue.begin();
                while(it != m_tasks_queue.end()){
                    task = *it;
                    m_tasks_queue.erase(it);
                    break;
                }
                has_more = (it != m_tasks_queue.end());
            }

            if(task){
                // 【核心】swapIn:切换到协程的上下文执行
                // 这行代码一执行,当前线程就开始跑 sing/dance/rap 的代码了
                task->swapIn();

                // swapIn 返回,说明协程要么结束了,要么 Yield 了
                if(task->getState() == sylar::Fiber::READY){
                    // 如果是 YieldToReady() 回来的,重新塞回队列
                    schedule(task);
                }
                // 如果是 TERM/EXCEPT/HOLD,就不入队了
            } else {
                // 队列为空,退出 run 循环(简化版,实际可用 idle 协程)
                if(!has_more) break;
            }
        }
    }

    void start(){
        std::cout << "Schedule start..." << std::endl;
        std::cout << "Init a thread poll" << std::endl;
        m_thread_poll.resize(m_poll_size);
        std::cout << "Schedule run..." << std::endl;
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i].reset(new sylar::Thread(
                std::bind(&Schedule::run,this),"THREAD_"+std::to_string(i)));
        }
    }
    
    void stop(){
        // 简化:等待所有线程结束
        for(size_t i = 0; i < m_poll_size; ++i){
            m_thread_poll[i]->join();
        }
        std::cout << "Schedule stop..." << std::endl; 
    }
    
    // 【改造2】schedule 接收 std::function,但内部包装成 Fiber 入队
    void schedule(std::function<void()> task){
        std::cout << "Push task into queue " << &task << std::endl;
        MutexType::Lock lock(m_mutex);
        m_tasks_queue.push_back(sylar::Fiber::ptr(new sylar::Fiber(task)));
    }

    // 【改造3】重载:直接调度一个已有的协程
    void schedule(sylar::Fiber::ptr fiber){
        MutexType::Lock lock(m_mutex);
        if(fiber && fiber->getState() != sylar::Fiber::EXEC){
            m_tasks_queue.push_back(fiber);
        }
    }

private:
    MutexType m_mutex;
    size_t m_poll_size = 0;
    std::vector<sylar::Thread::ptr> m_thread_poll;
    std::list<sylar::Fiber::ptr> m_tasks_queue;  // 【改造】队列里存的是协程!
    sylar::Thread* m_callerThread = 0;
};

// ============================================================
// main
// ============================================================

int main(){
    std::cout << "====Schedule(协程版)====" << std::endl;
    std::cout << "====Main start====" << std::endl;
    
    {
        auto start = std::chrono::high_resolution_clock::now();
       
        Schedule sc(2);               // 2 个线程
        sc.schedule(std::bind(&rap)); // 投递 3 个任务(会被包装成协程)
        sc.schedule(std::bind(&sing));
        sc.schedule(std::bind(&dance));
        sc.start();
        sc.stop();

        auto end = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double,std::milli> elapsed = end - start;
        std::cout << "Elapsed: " << (int)elapsed.count() << std::endl;
    }

    std::cout << "====Main end====" << std::endl;
    return 0;
}

简易版跟着过了一遍后我相信大伙对这个协程调度器都有了一定的初印象,那么现在再来看看sylar是怎么做的:

一、整体架构图

二、线程局部变量

先定义每个线程的调度器与主协程。

cpp 复制代码
static thread_local Scheduler* t_scheduler = nullptr;      // 当前线程绑定的调度器
static thread_local Fiber* t_scheduler_fiber = nullptr;    // 当前线程的主协程(调度协程)

作用:

  • t_scheduler:每个线程可以通过 Scheduler::GetThis() 快速获取当前所属的调度器,无需加锁。

  • t_scheduler_fiber:每个线程的主协程指针。在 use_caller 模式下,主线程的这个协程就是 m_rootFiber


三、Scheduler 类详解

3.1 定义(scheduler.h)

cpp 复制代码
class Scheduler {
public:
    typedef std::shared_ptr<Scheduler> ptr;
    typedef Mutex MutexType;

    // 线程数量,是否使用当前调用线程,协程调度器名称
    Scheduler(size_t threads = 1, bool use_caller = true, const std::string& name = "");

    virtual ~Scheduler();

    // 返回协程调度器名称
    const std::string& getName() const { return m_name;}

    /**
     * @brief 返回当前协程调度器
     */
    static Scheduler* GetThis();

    /**
     * @brief 返回当前协程调度器的调度协程
     */
    static Fiber* GetMainFiber();

    /**
     * @brief 启动协程调度器
     */
    void start();

    /**
     * @brief 停止协程调度器
     */
    void stop();

    /**
     * @brief 调度协程
     * @param[in] fc 协程或函数
     * @param[in] thread 协程执行的线程id,-1标识任意线程
     */
    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();
        }
    }

    /**
     * @brief 批量调度协程
     * @param[in] begin 协程数组的开始
     * @param[in] end 协程数组的结束
     */
    template<class InputIterator>
    void schedule(InputIterator begin, InputIterator end) {
        bool need_tickle = false;
        {
            MutexType::Lock lock(m_mutex);
            while(begin != end) {
                need_tickle = scheduleNoLock(&*begin, -1) || need_tickle;
                ++begin;
            }
        }
        if(need_tickle) {
            tickle();
        }
    }

    void switchTo(int thread = -1);
    std::ostream& dump(std::ostream& os);
protected:
    /**
     * @brief 通知协程调度器有任务了
     */

    virtual void tickle();
    /**
     * @brief 协程调度函数
     */
    void run();

    /**
     * @brief 返回是否可以停止
     */
    virtual bool stopping();

    /**
     * @brief 协程无任务可调度时执行idle协程
     */
    virtual void idle();

    /**
     * @brief 设置当前的协程调度器
     */
    void setThis();

    /**
     * @brief 是否有空闲线程
     */
    bool hasIdleThreads() { return m_idleThreadCount > 0;}
private:
    /**
     * @brief 协程调度启动(无锁)
     */
    template<class FiberOrCb>
    bool scheduleNoLock(FiberOrCb fc, int thread) {
        bool need_tickle = m_fibers.empty();
        FiberAndThread ft(fc, thread);
        if(ft.fiber || ft.cb) {
            m_fibers.push_back(ft);
        }
        return need_tickle;
    }
private:
    /**
     * @brief 协程/函数/线程组
     */
    struct FiberAndThread {
        /// 协程
        Fiber::ptr fiber;
        /// 协程执行函数
        std::function<void()> cb;
        /// 线程id
        int thread;

        /**
         * @brief 构造函数
         * @param[in] f 协程
         * @param[in] thr 线程id
         */
        FiberAndThread(Fiber::ptr f, int thr)
            :fiber(f), thread(thr) {
        }

        /**
         * @brief 构造函数
         * @param[in] f 协程指针
         * @param[in] thr 线程id
         * @post *f = nullptr
         */
        FiberAndThread(Fiber::ptr* f, int thr)
            :thread(thr) {
            fiber.swap(*f);
        }

        /**
         * @brief 构造函数
         * @param[in] f 协程执行函数
         * @param[in] thr 线程id
         */
        FiberAndThread(std::function<void()> f, int thr)
            :cb(f), thread(thr) {
        }

        /**
         * @brief 构造函数
         * @param[in] f 协程执行函数指针
         * @param[in] thr 线程id
         * @post *f = nullptr
         */
        FiberAndThread(std::function<void()>* f, int thr)
            :thread(thr) {
            cb.swap(*f);
        }

        /**
         * @brief 无参构造函数
         */
        FiberAndThread()
            :thread(-1) {
        }

        /**
         * @brief 重置数据
         */
        void reset() {
            fiber = nullptr;
            cb = nullptr;
            thread = -1;
        }
    };
private:
    /// Mutex
    MutexType m_mutex;
    /// 线程池
    std::vector<Thread::ptr> m_threads;
    /// 待执行的协程队列
    std::list<FiberAndThread> m_fibers;
    /// use_caller为true时有效, 调度协程
    Fiber::ptr m_rootFiber;
    /// 协程调度器名称
    std::string m_name;
protected:
    /// 协程下的线程id数组
    std::vector<int> m_threadIds;
    /// 线程数量
    size_t m_threadCount = 0;
    /// 工作线程数量
    std::atomic<size_t> m_activeThreadCount = {0};
    /// 空闲线程数量
    std::atomic<size_t> m_idleThreadCount = {0};
    /// 是否正在停止
    bool m_stopping = true;
    /// 是否自动停止
    bool m_autoStop = false;
    /// 主线程id(use_caller)
    int m_rootThread = 0;
};

class SchedulerSwitcher : public Noncopyable {
public:
    SchedulerSwitcher(Scheduler* target = nullptr);
    ~SchedulerSwitcher();
private:
    Scheduler* m_caller;
};

3.2 成员变量表

变量名 类型 默认值 含义
m_mutex MutexType 默认构造 保护 m_threadsm_fibers
m_threads vector<Thread::ptr> 线程池
m_fibers list<FiberAndThread> 待调度任务队列
m_rootFiber Fiber::ptr nullptr use_caller 模式下主线程的调度协程
m_name string "" 调度器名称
m_threadIds vector<int> 所有工作线程的ID列表
m_threadCount size_t 0 线程池线程数(不含 caller)
m_activeThreadCount atomic<size_t> 0 正在执行任务的线程数
m_idleThreadCount atomic<size_t> 0 空闲(idle)线程数
m_stopping bool true 是否正在停止
m_autoStop bool false 是否自动停止
m_rootThread int 0 use_caller 的主线程ID,否则-1

3.3 构造函数(scheduler.cc

cpp 复制代码
Scheduler::Scheduler(size_t threads, bool use_caller, const std::string& name)
    :m_name(name) {
    SYLAR_ASSERT(threads > 0);
​
    if(use_caller) {
        sylar::Fiber::GetThis();    // 确保当前线程已创建主协程
        --threads;                   // 主线程也要参与调度,线程池少创建一个
​
        SYLAR_ASSERT(GetThis() == nullptr);
        t_scheduler = this;
​
        m_rootFiber.reset(new Fiber(std::bind(&Scheduler::run, this), 0, true));
        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;
}

逐行拆解:

  1. SYLAR_ASSERT(threads > 0):必须至少有一个线程参与调度。

  2. Fiber::GetThis():如果当前线程还没有创建主协程,先创建。这是为了后续 m_rootFiber 的切换操作能够正常进行。

  3. --threads:因为主线程(caller)也要跑 run(),线程池中只需要创建 threads - 1 个。

  4. t_scheduler = this:将当前调度器绑定到主线程的 thread_local 变量。

  5. m_rootFiber.reset(new Fiber(std::bind(&Scheduler::run, this), 0, true)):创建主调度协程。

    • 入口函数是 Scheduler::run

    • 0 表示栈大小使用默认

    • true 表示这是主协程(不会被自动销毁)

  6. t_scheduler_fiber = m_rootFiber.get():主线程的主协程就是这个 m_rootFiber

  7. m_rootThread = sylar::GetThreadId():记录主线程ID。

  8. else 分支:use_caller = false 时,主线程不参调度,m_rootThread = -1


3.4 析构函数(scheduler.cc

cpp 复制代码
Scheduler::~Scheduler() {
    SYLAR_ASSERT(m_stopping);
    if(GetThis() == this) {
        t_scheduler = nullptr;
    }
}
  • 析构时要求调度器已经处于停止状态。

  • 如果当前线程绑定的调度器就是 this,将其解绑(置为 nullptr)。


3.5 GetThis() / GetMainFiber()scheduler.cc

cpp 复制代码
Scheduler* Scheduler::GetThis() {
    return t_scheduler;
}
​
Fiber* Scheduler::GetMainFiber() {
    return t_scheduler_fiber;
}
  • 直接返回 thread_local 变量,O(1) 且无锁

  • 这是整个框架中非常核心的设计:每个线程通过 TLS 知道自己属于哪个调度器。


3.6 start() ------ 启动调度器(scheduler.cc

cpp 复制代码
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) {
        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());
    }
    lock.unlock();
}

关键逻辑:

  1. 加锁,检查 m_stopping,防止重复启动。

  2. 断言 m_threads 为空,确保是第一次启动。

  3. 创建 m_threadCountThread 对象,每个线程的入口都是 Scheduler::run

  4. 将线程ID记录到 m_threadIds

  5. lock.unlock():尽早释放锁,Thread 启动后会立即竞争 m_fibers

注意: 如果 use_caller = true,主线程的 m_rootFiber 并不会在这里执行。start() 只负责启动线程池中的线程。主线程只会在 stop() 中手动 call() 进入 run()


3.7 stop() ------ 停止调度器(scheduler.cc

cpp 复制代码
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;
    // 唤醒所有可能阻塞在 idle 的线程
    for(size_t i = 0; i < m_threadCount; ++i) {
        tickle();
    }
    if(m_rootFiber) {
        tickle();
    }
​
    // 主线程进入 run()(如果是 use_caller)
    if(m_rootFiber) {
        if(!stopping()) {
            m_rootFiber->call();
        }
    }
​
    // 等待所有线程结束
    std::vector<Thread::ptr> thrs;
    {
        MutexType::Lock lock(m_mutex);
        thrs.swap(m_threads);
    }
    for(auto& i : thrs) {
        i->join();
    }
}

逐段拆解:

第一段:快速停止路径

  • 如果 use_caller=true,且线程池为空(m_threadCount == 0),且主协程已经结束,说明没有任务要执行,可以直接停止。

  • stopping() 会检查 m_autoStop && m_stopping && m_fibers.empty() && m_activeThreadCount == 0

第二段:唤醒所有线程

  • 设置 m_stopping = true,让 run() 中的循环有机会退出。

  • 对每个线程调用 tickle(),唤醒可能阻塞在 idle() 中的线程。

  • use_caller 模式下也对 m_rootFiber tickle。

第三段:主线程参与调度

  • m_rootFiber->call():主线程进入 run() 开始调度。注意用的是 call() 而不是 swapIn(),因为主协程是全新的,还没有执行过。

  • 主线程在这里会和其他工作线程一样,从 m_fibers 中取任务执行。

第四段:等待线程结束

  • m_threads 交换到局部变量 thrs,避免在 join() 时持有锁。

  • 逐个 join() 等待所有工作线程退出。


3.8 schedule() ------ 提交任务(scheduler.h)

cpp 复制代码
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();
    }
}

逻辑:

  1. 加锁,调用 scheduleNoLock() 将任务放入 m_fibers

  2. scheduleNoLock() 返回 need_tickle:如果任务队列之前是空的,则需要唤醒 idle 线程。

  3. 解锁后,如果需要,调用 tickle()

thread 参数:

  • -1:任意线程都可以执行。

  • >=0:只有指定线程ID可以执行(用于线程亲和性)。


3.9 scheduleNoLock() ------ 无锁提交(scheduler.h)

cpp 复制代码
template<class FiberOrCb>
bool scheduleNoLock(FiberOrCb fc, int thread) {
    bool need_tickle = m_fibers.empty();   // 如果队列为空,需要 tickle
    FiberAndThread ft(fc, thread);
    if(ft.fiber || ft.cb) {
        m_fibers.push_back(ft);
    }
    return need_tickle;
}
  • need_tickle添加任务之前就判断了。如果原来队列非空,说明已经有线程在工作或即将工作,不需要额外 tickle。

  • FiberAndThread 的构造函数会自动处理 Fiber::ptrFiber::ptr*std::function<void()>std::function<void()>* 四种类型。


3.10 run() ------ 核心调度循环(scheduler.cc

这是整个调度器最核心的函数,每个工作线程都在执行它。

cpp 复制代码
void Scheduler::run() {
    SYLAR_LOG_DEBUG(g_logger) << m_name << " run";
    // hook还没讲,暂时搁置
    set_hook_enable(true);    // 启用 hook(让阻塞系统调用变成协程切换)
    setThis();                // t_scheduler = this
​
    // 如果不是主线程,设置本线程的主协程
    if(sylar::GetThreadId() != m_rootThread) {
        t_scheduler_fiber = Fiber::GetThis().get();
    }
​
    Fiber::ptr idle_fiber(new Fiber(std::bind(&Scheduler::idle, this)));
    /* 如果任务是 `std::function`(不是现成的 `Fiber`),
    sylar 会把它包成协程。`cb_fiber` 对象复用,避免反复 `new/delete`。*/
    Fiber::ptr cb_fiber;
    FiberAndThread ft;
​
    while(true) {
        ft.reset();
        bool tickle_me = false;
        bool is_active = false;
​
        // ========== 1. 从任务队列取任务 ==========
        {
            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;   // 有其他线程的任务,需要 tickle 让正确线程来取
                    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;   // 本线程变为 active 状态
                is_active = true;
                break;
            }
            tickle_me |= it != m_fibers.end();  // 队列还有任务,tickle 其他线程
        }
​
        if(tickle_me) {
            tickle();
        }
​
        // ========== 2. 执行任务 ==========
        if(ft.fiber && (ft.fiber->getState() != Fiber::TERM
                        && ft.fiber->getState() != Fiber::EXCEPT)) {
            // 2a. 执行协程
            ft.fiber->swapIn();
            --m_activeThreadCount;
​
            if(ft.fiber->getState() == Fiber::READY) {
                schedule(ft.fiber);     // 协程 yield 时设为 READY,重新入队
            } else if(ft.fiber->getState() != Fiber::TERM
                    && ft.fiber->getState() != Fiber::EXCEPT) {
                ft.fiber->m_state = Fiber::HOLD;   // 其他状态设为 HOLD(等待被唤醒)
                // 这是为 IOManager / Hook / 定时器 准备的
            }
            ft.reset();
​
        } else if(ft.cb) {
            // 2b. 执行回调函数:包装为协程再执行
            if(cb_fiber) {
                cb_fiber->reset(ft.cb);   // 复用已有的 cb_fiber
            } else {
                cb_fiber.reset(new Fiber(ft.cb));
            }
            ft.reset();
            cb_fiber->swapIn();
            --m_activeThreadCount;
​
            if(cb_fiber->getState() == Fiber::READY) {
                schedule(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 {
            // ========== 3. 没有任务,进入 idle ==========
            if(is_active) {
                --m_activeThreadCount;   // 之前标记了 active 但没取到有效任务
                continue;
            }
            if(idle_fiber->getState() == Fiber::TERM) {
                break;   // idle 协程结束了,run() 循环退出
            }
​
            ++m_idleThreadCount;
            idle_fiber->swapIn();        // 切换到 idle 协程(内部会 YieldToHold)
            --m_idleThreadCount;
​
            if(idle_fiber->getState() != Fiber::TERM
                    && idle_fiber->getState() != Fiber::EXCEPT) {
                idle_fiber->m_state = Fiber::HOLD;
            }
        }
    }
}

任务状态流转总结:

任务类型 swapIn 后状态 后续处理
ft.fiber READY 重新 schedule() 入队
ft.fiber TERM / EXCEPT 丢弃
ft.fiber 其他 设为 HOLD
ft.cb READY 重新 schedule(),释放 cb_fiber
ft.cb TERM / EXCEPT reset(nullptr)
ft.cb 其他 设为 HOLD,释放 cb_fiber

考虑到此段函数非常复杂,如果基础不好看着非常吃力,我们来与简易版进行一些对比,帮助大家理解:

第一部分:从任务队列取任务

sylar 版:

cpp 复制代码
{
    MutexType::Lock lock(m_mutex);
    auto it = m_fibers.begin();
    while(it != m_fibers.end()) {
        // 【A】线程亲和性检查
        if(it->thread != -1 && it->thread != sylar::GetThreadId()) {
            ++it;
            tickle_me = true;   // 有任务不属于我,tickle 让正确线程来取
            continue;
        }
​
        // 【B】防重复执行检查
        if(it->fiber && it->fiber->getState() == Fiber::EXEC) {
            ++it;
            continue;
        }
​
        ft = *it;
        m_fibers.erase(it++);
        ++m_activeThreadCount;   // 【C】本线程标记为"工作中"
        is_active = true;
        break;
    }
    tickle_me |= it != m_fibers.end();  // 【D】队列还有剩余任务?
}
​
if(tickle_me) {
    tickle();  // 【E】唤醒其他可能在 idle 的线程
}

简易版:

cpp 复制代码
{
    MutexType::Lock lock(m_mutex);
    auto it = m_tasks_queue.begin();
    while(it != m_tasks_queue.end()){
        task = *it;
        m_tasks_queue.erase(it);
        break;
    }
    has_more = (it != m_tasks_queue.end());
}

A. 线程亲和性(thread != -1

sylar 的 schedule(fc, thread_id) 允许指定第几个线程执行。如果当前线程不是目标线程,就跳过,tickle_me = true 通知正确线程来取。

简易版没有这个功能,所有线程随便抢。

B. EXEC 状态过滤

sylar 在加锁遍历队列时,会跳过 state == EXEC 的协程。为什么?

理论上不应该出现(一个协程只能在一个线程上 EXEC),但这是防御性编程 :万一某个协程被重复 schedule 了两次,避免两个线程同时 swapIn 同一个协程导致栈错乱。

简易版没有这个保护。

C/D/E. m_activeThreadCount + tickle()

sylar 维护了两个原子计数器:

  • m_activeThreadCount:正在执行任务的线程数

  • m_idleThreadCount:正在 idle 的线程数

当队列里还有任务it != m_fibers.end()),但当前线程因为亲和性取不了时,tickle() 会唤醒一个 idle 线程来处理。

简易版没有 tickle 机制。如果所有线程都在忙,新任务入队后,忙线程不会主动让出,新任务可能延迟处理。


第二部分A:执行的是协程

sylar 版:

cpp 复制代码
if(ft.fiber && (ft.fiber->getState() != Fiber::TERM
                && ft.fiber->getState() != Fiber::EXCEPT)) {
    ft.fiber->swapIn();
    --m_activeThreadCount;
​
    if(ft.fiber->getState() == Fiber::READY) {
        schedule(ft.fiber);           // 【A】YieldToReady → 重新入队
    } else if(ft.fiber->getState() != Fiber::TERM
              && ft.fiber->getState() != Fiber::EXCEPT) {
        ft.fiber->m_state = Fiber::HOLD;  // 【B】其他状态 → HOLD
    }
    ft.reset();
}

简易版:

cpp 复制代码
if(task){
    task->swapIn();
    if(task->getState() == sylar::Fiber::READY){
        schedule(task);   // 只有 READY 会重新入队
    }
}

A. READY 状态的精确处理

两者逻辑一样:swapIn() 返回后如果状态是 READY,说明协程调用了 YieldToReady(),应该重新入队等待下次调度。

B. HOLD 状态的区分处理

sylar 多了一步:

cpp 复制代码
ft.fiber->m_state = Fiber::HOLD;

这是为 IOManager / Hook / 定时器 准备的:

  • READY = 我自己让出的,我想继续排队等 CPU。

  • HOLD = 我在等某个外部事件(比如 epoll_wait 返回、定时器到期、socket 可读)。

简易版没有区分 HOLD 和 READY。在纯计算任务场景下没区别,但在 IO 场景下,HOLD 的协程不应该被普通调度,而应该被事件触发后专门唤醒。

C. ft.reset()

sylar 显式清空 ftFiberAndThread 结构体),释放智能指针引用,避免协程对象被 ft 意外持有。


第二部分B:执行的是回调函数

sylar 版:

cpp 复制代码
} else if(ft.cb) {
    if(cb_fiber) {
        cb_fiber->reset(ft.cb);   // 【A】复用已有的协程对象,只换函数
    } else {
        cb_fiber.reset(new Fiber(ft.cb));  // 【B】第一次:新建协程
    }
    ft.reset();
    cb_fiber->swapIn();
    --m_activeThreadCount;
​
    if(cb_fiber->getState() == Fiber::READY) {
        schedule(cb_fiber);
        cb_fiber.reset();         // 【C】重新入队后,不再本地持有
    } else if(cb_fiber->getState() == Fiber::EXCEPT
              || cb_fiber->getState() == Fiber::TERM) {
        cb_fiber->reset(nullptr); // 【D】结束/异常,清空回调复用
    } else {
        cb_fiber->m_state = Fiber::HOLD;
        cb_fiber.reset();
    }
}

简易版:

cpp 复制代码
// schedule() 时就把 std::function 包成了 Fiber
void schedule(std::function<void()> task){
    m_tasks_queue.push_back(sylar::Fiber::ptr(new sylar::Fiber(task)));
}

A/B. cb_fiber 复用机制

sylar 的队列里存的是 FiberAndThread,它可以是 fiber 也可以是 cb。如果是 cb,sylar 不在 schedule 时包装 ,而是在 run() 执行时才包装。并且用 cb_fiber 这个成员变量反复复用 同一个 Fiber 对象,只通过 reset(ft.cb) 换绑定的函数。

好处 :避免每次 schedule(std::function)new Fiber() + malloc(128KB栈)。高频调度函数时,复用能大幅减少内存分配开销。

简易版在 schedule() 里就 new Fiber(task),每次都有内存分配。

C/D. 状态分支更细致

状态 sylar 处理 含义
READY schedule(cb_fiber); cb_fiber.reset(); yield 了,交给队列,本地不持有
TERM reset(nullptr) 正常结束,清空函数,对象留着下次复用
EXCEPT reset(nullptr) 异常结束,同样清空
其他 设为 HOLDreset() 等事件唤醒

简易版只处理了 READY,没有复用、没有 HOLD、没有异常处理第三部分


第四部分:空闲处理(idle 协程)

sylar 版:

cpp 复制代码
} else {
    if(is_active) {
        --m_activeThreadCount;   // 【A】之前误标记了 active,修正
        continue;
    }
    if(idle_fiber->getState() == Fiber::TERM) {
        break;                    // 【B】idle 协程结束了,run() 退出
    }
​
    ++m_idleThreadCount;
    idle_fiber->swapIn();         // 【C】切换到 idle 协程(内部 YieldToHold)
    --m_idleThreadCount;
​
    if(idle_fiber->getState() != Fiber::TERM
       && idle_fiber->getState() != Fiber::EXCEPT) {
        idle_fiber->m_state = Fiber::HOLD;
    }
}

简易版:

cpp 复制代码
} else {
    if(!has_more) break;  // 没任务就直接退出 run()
}

A. is_active 修正

前面取任务时 ++m_activeThreadCount,但如果取出的 ft 既没有有效 fiber 也没有有效 cb(比如 fiber 是 TERM 状态),就会走到 else 分支。此时 is_active 为 true,需要 --m_activeThreadCount 修正计数。

B/C. idle 协程不退,而是反复被 tickle 唤醒

sylar 的 idle 协程是一个无限循环

cpp 复制代码
void Scheduler::idle() {
    while(!stopping()) {
        Fiber::YieldToHold();   // 让出,等待 tickle 唤醒
    }
}

线程没有任务时,不会退出 run(),而是 swapIn(idle_fiber)YieldToHold() → 切回 run() → 再次检查队列。如果队列还是空,又会进入 idle。

为什么这样做?

如果像简易版那样直接 break 退出 run(),线程就结束了。但 sylar 的 run() 是线程入口函数,线程一退出就真没了。下次有新任务时,只能依赖其他线程或重新创建线程。

sylar 的方式是:线程不死,只是休眠 。通过 tickle()(比如往 pipe 写一个字节、或 notify 条件变量)唤醒 idle 协程,idle 协程 swapOut 回来后,run() 的 while 循环继续检查队列,取到新任务执行。

在 IOManager 里,idle 协程的 YieldToHold() 会换成 epoll_wait(),线程真正阻塞在内核,不消耗 CPU。


3.11 tickle() / stopping() / idle()

tickle()scheduler.cc
cpp 复制代码
void Scheduler::tickle() {
    SYLAR_LOG_INFO(g_logger) << "tickle";
}
  • 基类版本是空实现(只打日志)。

  • IOManager 会覆盖此方法,用 pipe 写数据来唤醒阻塞在 epoll_wait 的线程。

stopping()scheduler.cc
cpp 复制代码
bool Scheduler::stopping() {
    MutexType::Lock lock(m_mutex);
    return m_autoStop && m_stopping
        && m_fibers.empty() && m_activeThreadCount == 0;
}

停止条件(必须同时满足):

  1. m_autoStop = true:已经调用了 stop()

  2. m_stopping = true:已经设置了停止标志。

  3. m_fibers.empty():没有待执行任务。

  4. m_activeThreadCount == 0:没有线程正在执行任务。

idle()scheduler.cc
cpp 复制代码
void Scheduler::idle() {
    SYLAR_LOG_INFO(g_logger) << "idle";
    while(!stopping()) {
        sylar::Fiber::YieldToHold();
    }
}
  • 空闲协程的入口函数。

  • 只要还没停止,就不断 YieldToHold(),让出 CPU。

  • 每次 YieldToHold() 会切回调度协程(run() 中的循环),再次检查任务队列。

  • 如果 stopping() 返回 true,idle() 结束,idle_fiber 状态变为 TERMrun() 的循环也随之退出。


3.12 switchTo() ------ 协程迁移(scheduler.cc

cpp 复制代码
void Scheduler::switchTo(int thread) {
    SYLAR_ASSERT(Scheduler::GetThis() != nullptr);
    if(Scheduler::GetThis() == this) {
        if(thread == -1 || thread == sylar::GetThreadId()) {
            return;   // 已经是目标调度器和线程,无需切换
        }
    }
    schedule(Fiber::GetThis(), thread);   // 将当前协程重新调度到目标线程
    Fiber::YieldToHold();                  // 立即让出,等待被调度
}
  • 将当前协程重新 schedule 到指定的调度器和线程。

  • 然后 YieldToHold() 让出执行权。

  • SchedulerSwitcher 的 RAII 类就是利用这个函数实现的。


四、FiberAndThread 结构体详解

4.1 定义(scheduler.h)

cpp 复制代码
struct FiberAndThread {
    Fiber::ptr fiber;
    std::function<void()> cb;
    int thread;
​
    FiberAndThread(Fiber::ptr f, int thr)
        :fiber(f), thread(thr) {}
​
    FiberAndThread(Fiber::ptr* f, int thr)
        :thread(thr) {
        fiber.swap(*f);   // 转移所有权,*f 被置空
    }
​
    FiberAndThread(std::function<void()> f, int thr)
        :cb(f), thread(thr) {}
​
    FiberAndThread(std::function<void()>* f, int thr)
        :thread(thr) {
        cb.swap(*f);      // 转移所有权,*f 被置空
    }
​
    FiberAndThread()
        :thread(-1) {}
​
    void reset() {
        fiber = nullptr;
        cb = nullptr;
        thread = -1;
    }
};

设计要点:

  • 同时支持协程(fiber)和回调函数(cb)两种任务形式。

  • 指针版本的构造函数使用 swap() 转移所有权,避免 shared_ptr 引用计数的额外增减。

  • swap() 直接交换 shared_ptr 内部的两个裸指针(对象指针 + 控制块指针),既避免了原子加减的开销,又实现了所有权的强制转移。

  • thread = -1 表示不指定线程,任意线程都可以执行。


五、SchedulerSwitcher 类详解

5.1 定义(scheduler.h)

cpp 复制代码
class SchedulerSwitcher : public Noncopyable {
public:
    SchedulerSwitcher(Scheduler* target = nullptr);
    ~SchedulerSwitcher();
private:
    Scheduler* m_caller;
};

5.2 实现(scheduler.cc

cpp 复制代码
SchedulerSwitcher::SchedulerSwitcher(Scheduler* target) {
    m_caller = Scheduler::GetThis();
    if(target) {
        target->switchTo();
    }
}
​
SchedulerSwitcher::~SchedulerSwitcher() {
    if(m_caller) {
        m_caller->switchTo();
    }
}

RAII 设计:

  • 构造时:保存原来的调度器,切换到 target 调度器。

  • 析构时:切回原来的调度器。

  • 典型用法:临时把当前协程切换到另一个调度器上执行一段代码,离开作用域后自动恢复。


六、批量调度 schedule(begin, end)

cpp 复制代码
template<class InputIterator>
void schedule(InputIterator begin, InputIterator end) {
    bool need_tickle = false;
    {
        MutexType::Lock lock(m_mutex);
        while(begin != end) {
            need_tickle = scheduleNoLock(&*begin, -1) || need_tickle;
            ++begin;
        }
    }
    if(need_tickle) {
        tickle();
    }
}
  • 批量提交,只对 m_mutex 加一次锁。

  • &*begin 取迭代器指向元素的地址,传入指针版本构造函数转移所有权。

  • need_tickle 的逻辑:只要有一个任务是队列空时加入的,就需要 tickle。


结语

今天带着大家将协程调度器串了一遍,相信大家对于此块的内容都有了一定的理解,我们调度器在main函数构造,然后用schedule提交任务,start运行绑定run和线程池中的线程,run再去运行任务,最后stop加上主线程一起干活。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️ 封面自取 ⭐️---