【项目篇】从零手写高并发服务器(七):定时器TimerWheel与线程池

文章目录

从零手写高并发服务器(七):定时器TimerWheel与线程池

💬 开篇:上一篇我们实现了EventLoop,但它还缺两个重要能力:定时任务管理和多线程支持。本篇我们把前置知识中学过的时间轮集成到EventLoop中,然后实现EventLoopThread和EventLoopThreadPool,让服务器具备多线程处理能力。

👍 点赞、收藏与分享:定时器用于连接超时管理,线程池用于并发处理,都是高并发服务器的核心组件。

🚀 循序渐进:TimerWheel集成 → EventLoopThread → EventLoopThreadPool → 测试验证。


一、TimerWheel定时器集成到EventLoop

1.1 回顾时间轮原理

前置知识篇我们已经实现过时间轮了,这里要做的是:

  1. 把TimerWheel集成到EventLoop中
  2. timerfd 驱动时间轮的tick(每秒触发一次)
  3. 所有定时器操作都必须在EventLoop所在线程中执行(线程安全)
bash 复制代码
EventLoop + TimerWheel 的关系:

  EventLoop
  ┌──────────────────────────────────┐
  │  Poller (epoll)                   │
  │    ├── eventfd (唤醒用)            │
  │    ├── timerfd (每秒触发一次)       │
  │    └── 其他fd...                   │
  │                                    │
  │  TimerWheel                        │
  │    └── 每次timerfd触发 → tick前进    │
  │                                    │
  │  TaskQueue (任务队列)               │
  └──────────────────────────────────┘

1.2 需要的头文件

server.hpp 顶部添加:

cpp 复制代码
#include <sys/timerfd.h>
#include <thread>
#include <mutex>
#include <condition_variable>

1.3 TimerTask类------定时任务的封装

首先我们需要一个类来封装「定时任务」,这个对象在析构时会自动执行任务:

cpp 复制代码
// ==================== 定时任务类 ====================
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;

class TimerTask {
private:
    uint64_t _id;           // 定时器任务ID
    uint32_t _timeout;      // 定时任务的超时时间(多少秒后执行)
    bool _canceled;         // false-正常执行  true-被取消了
    TaskFunc _task_cb;      // 定时器要执行的回调函数
    ReleaseFunc _release;   // 定时器被释放时的清理函数
    
public:
    // 构造函数
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb)
        : _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}
    
    // 析构函数:如果没被取消,就执行任务
    ~TimerTask() {
        if (_release) _release();      // 先调清理函数
        if (!_canceled) _task_cb();    // 没被取消就执行任务
    }
    
    // 取消这个定时任务
    void Cancel() { _canceled = true; }
    
    // 设置清理函数
    void SetRelease(const ReleaseFunc &cb) { _release = cb; }
    
    // 获取延迟时间
    uint32_t DelayTime() { return _timeout; }
};

关键点讲解:

  • 析构函数中执行 _task_cb():当这个对象被删除时,任务自动执行
  • _canceled 标志:如果被取消了,析构时就不执行任务
  • _release 函数:用于从时间轮的 map 中移除这个任务的记录

1.4 TimerWheel类------时间轮实现

cpp 复制代码
// ==================== 时间轮定时器 ====================
class TimerWheel {
private:
    // 类型别名定义
    using WeakTask = std::weak_ptr<TimerTask>;     // weak_ptr:观测用
    using PtrTask = std::shared_ptr<TimerTask>;    // shared_ptr:管理对象生命周期
    
    int _tick;                              // 当前秒针位置
    int _capacity;                          // 表盘大小(最大延迟秒数)
    std::vector<std::vector<PtrTask>> _wheel;  // 时间轮:每个位置存一个任务列表
    std::unordered_map<uint64_t, WeakTask> _timers;  // id映射:快速查找任务
    
    // EventLoop相关
    EventLoop *_loop;
    int _timerfd;                           // timerfd描述符
    std::unique_ptr<Channel> _timer_channel;  // timerfd的事件管理
    
private:
    // 从映射表中移除定时任务记录
    void RemoveTimer(uint64_t id) {
        auto it = _timers.find(id);
        if (it != _timers.end()) {
            _timers.erase(it);
        }
    }
    
    // 创建timerfd(内核定时器)
    static int CreateTimerfd() {
        int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
        if (timerfd < 0) {
            ERR_LOG("TIMERFD CREATE FAILED!");
            abort();
        }
        
        // 设置定时器:每秒触发一次
        struct itimerspec itime;
        itime.it_value.tv_sec = 1;       // 第一次超时:1秒后
        itime.it_value.tv_nsec = 0;
        itime.it_interval.tv_sec = 1;    // 之后每次超时间隔:1秒
        itime.it_interval.tv_nsec = 0;
        timerfd_settime(timerfd, 0, &itime, NULL);
        
        return timerfd;
    }
    
    // 读取timerfd(消费超时事件)
    int ReadTimerfd() {
        uint64_t times;
        int ret = read(_timerfd, &times, 8);
        if (ret < 0) {
            ERR_LOG("READ TIMERFD FAILED!");
            abort();
        }
        return times;  // 返回超时次数
    }
    
    // 执行一步tick(秒针前进一格)
    void RunTimerTask() {
        _tick = (_tick + 1) % _capacity;  // 秒针循环前进
        _wheel[_tick].clear();  // 清空这个位置的任务,任务对象释放 → 析构 → 执行
    }
    
    // timerfd可读事件的回调函数
    void OnTime() {
        // 读出超时了几次(比如一下超时了2次)
        int times = ReadTimerfd();
        
        // 对于每次超时,都执行一步tick
        for (int i = 0; i < times; i++) {
            RunTimerTask();
        }
    }
    
    // 内部接口:添加定时任务
    void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
        // 创建任务对象(shared_ptr管理)
        PtrTask pt(new TimerTask(id, delay, cb));
        
        // 设置清理函数:任务释放时从映射表中移除
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        
        // 存到映射表(用weak_ptr)
        _timers[id] = WeakTask(pt);
        
        // 计算这个任务应该放在时间轮的哪个位置
        // 当前位置 + 延迟秒数 = 应该放的位置
        int pos = (_tick + delay) % _capacity;
        
        // 把任务放到时间轮
        _wheel[pos].push_back(pt);
    }
    
    // 内部接口:刷新定时任务(延迟时间重新计算)
    void TimerRefreshInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) {
            return;  // 找不到这个任务,可能已经执行了
        }
        
        // 从weak_ptr恢复出shared_ptr
        PtrTask pt = it->second.lock();
        if (!pt) {
            return;  // 对象已经被释放了
        }
        
        // 重新计算位置并加入时间轮
        int delay = pt->DelayTime();
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
    }
    
    // 内部接口:取消定时任务
    void TimerCancelInLoop(uint64_t id) {
        auto it = _timers.find(id);
        if (it == _timers.end()) {
            return;
        }
        
        PtrTask pt = it->second.lock();
        if (pt) {
            pt->Cancel();  // 标记为已取消
        }
    }
    
public:
    // 构造函数
    TimerWheel(EventLoop *loop)
        : _loop(loop),  // ← 注意:_loop 要放在前面,后面要用它
          _capacity(60), 
          _tick(0), 
          _wheel(_capacity),
          _timerfd(CreateTimerfd()),
          _timer_channel(new Channel(_loop, _timerfd))
    {
        // 给timerfd设置可读事件回调
        _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
        
        // 启动timerfd的读事件监控
        _timer_channel->EnableRead();
    }
    
    // 对外接口1:添加定时任务(线程安全)
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
    
    // 对外接口2:刷新定时任务(线程安全)
    void TimerRefresh(uint64_t id);
    
    // 对外接口3:取消定时任务(线程安全)
    void TimerCancel(uint64_t id);
    
    // 查询定时任务是否存在
    bool HasTimer(uint64_t id) {
        auto it = _timers.find(id);
        return (it != _timers.end());
    }
};

时间轮工作流程图解:

bash 复制代码
初始状态(_tick=0):
_wheel[0] [ ]
_wheel[1] [ ]
_wheel[2] [Task1(delay=2)] [Task2(delay=2)]
_wheel[3] [ ]
...

1秒后 (_tick=1):
_wheel[0] [ ]
_wheel[1] [ ]
_wheel[2] [ ]
_wheel[3] [Task1] [Task2]  ← Task1和Task2都往前走了一格
...

2秒后 (_tick=2):
_wheel[0] [ ]
_wheel[1] [ ]
_wheel[2] [ ]
_wheel[3] [ ]
_wheel[0] [Task1] [Task2]  ← 继续往前走,Task1和Task2析构 → 执行!
...

1.5 TimerWheel对外接口的实现

这三个方法要放到 EventLoop 完整定义之后(因为要调用 _loop->RunInLoop):

cpp 复制代码
// 这段代码要放在 EventLoop 类定义之后

inline void TimerWheel::TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) {
    // 把实际添加的工作放到EventLoop线程中执行(线程安全)
    _loop->RunInLoop(std::bind(&TimerWheel::TimerAddInLoop, this, id, delay, cb));
}

inline void TimerWheel::TimerRefresh(uint64_t id) {
    _loop->RunInLoop(std::bind(&TimerWheel::TimerRefreshInLoop, this, id));
}

inline void TimerWheel::TimerCancel(uint64_t id) {
    _loop->RunInLoop(std::bind(&TimerWheel::TimerCancelInLoop, this, id));
}

为什么这样做?

  • TimerAddInLoop 等方法操作 _wheel_timers
  • 这些操作必须在 EventLoop 线程中执行(因为 EventLoop 中 RunAllTask() 也在操作这些数据)
  • RunInLoop 保证无论从哪个线程调用,都能在正确的线程中执行

1.6 在EventLoop中集成TimerWheel

修改 EventLoop 类,添加 TimerWheel 成员:

cpp 复制代码
class EventLoop {
private:
    using Functor = std::function<void()>;
    std::thread::id _thread_id;
    int _event_fd;
    std::unique_ptr<Channel> _event_channel;
    Poller _poller;
    std::vector<Functor> _tasks;
    std::mutex _mutex;
    std::unique_ptr<TimerWheel> _timer_wheel;  // ← 新增:定时器
    
public:
    // 构造函数
    EventLoop()
        : _thread_id(std::this_thread::get_id()),
          _event_fd(CreateEventFd()),
          _event_channel(new Channel(this, _event_fd)),
          _timer_wheel(new TimerWheel(this))  // ← 初始化定时器
    {
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
        _event_channel->EnableRead();
    }
    
    // ... 之前的所有方法保持不变 ...
    
    // 新增:定时器对外接口
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb) {
        return _timer_wheel->TimerAdd(id, delay, cb);
    }
    
    void TimerRefresh(uint64_t id) {
        return _timer_wheel->TimerRefresh(id);
    }
    
    void TimerCancel(uint64_t id) {
        return _timer_wheel->TimerCancel(id);
    }
    
    bool HasTimer(uint64_t id) {
        return _timer_wheel->HasTimer(id);
    }
};

二、EventLoopThread------线程与事件循环的绑定

2.1 为什么需要EventLoopThread?

EventLoop必须在自己的线程中运行。EventLoopThread就是把EventLoop和线程绑定在一起的类:创建一个新线程,在这个线程内部实例化EventLoop并运行它。

bash 复制代码
EventLoopThread的作用:

  主线程(main)              子线程(EventLoopThread)
       │                            │
       │ new LoopThread()           │
       │     │                      │ 创建一个新线程
       │     │                      │     ↓
       │     │                  EventLoop loop
       │     │                      ↓
       │     │                  loop.Start()  ← 进入事件循环
       │     │                      ↓(阻塞)
       │ GetLoop()  ← 需要等待!      │ 异步事件处理
       │     ↓                      │
       │ 返回loop指针 ← 同步点          │

同步问题: 主线程调用 GetLoop() 时,子线程可能还在创建 EventLoop,还没赋值给 _loop。所以需要用条件变量来同步。

2.2 LoopThread类实现

cpp 复制代码
// ==================== EventLoopThread ====================
class LoopThread {
private:
    std::mutex _mutex;              // 保护_loop的访问
    std::condition_variable _cond;  // 条件变量:用于同步
    EventLoop *_loop;               // EventLoop指针,子线程中创建
    std::thread _thread;            // 子线程对象
    
private:
    // 线程入口函数:这个函数在新线程中执行
    void ThreadEntry() {
        // 1. 在新线程中创建 EventLoop 对象
        EventLoop loop;
        
        // 2. 把指针赋值给成员变量,并通知等待者
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _loop = &loop;  // ← 赋值
            _cond.notify_all();  // ← 通知所有等待的线程
        }
        
        // 3. 启动事件循环(这是一个无限循环)
        loop.Start();
        
        // ← 代码永远执行不到这里,除非 loop.Start() 退出
    }
    
public:
    // 构造函数:创建新线程
    LoopThread() 
        : _loop(NULL), 
          _thread(std::thread(&LoopThread::ThreadEntry, this))
          //                    ↑ 绑定线程入口函数
    {
    }
    
    // 获取这个线程的 EventLoop
    // 这个方法会阻塞,直到子线程创建好EventLoop
    EventLoop *GetLoop() {
        EventLoop *loop = NULL;
        
        {
            std::unique_lock<std::mutex> lock(_mutex);
            
            // 等待,直到 _loop 不为 NULL
            _cond.wait(lock, [&](){ return _loop != NULL; });
            //  ↑ 第二个参数是 lambda 谓词
            //    只有谓词返回 true,wait 才返回
            //    否则会继续等待
            
            loop = _loop;
        }
        
        return loop;
    }
};

等待机制详解:

cpp 复制代码
_cond.wait(lock, [&](){ return _loop != NULL; });

// 等价于:
while (_loop == NULL) {
    // 释放 lock,进入等待
    // 当被 notify_all() 唤醒时,重新获取 lock
    // 检查谓词,如果还是 false,继续等待
    // 直到谓词返回 true,才真正返回
}

三、EventLoopThreadPool线程池

3.1 线程池的作用

线程池管理多个LoopThread,实现主从Reactor模型:

bash 复制代码
主从Reactor模型:

  主线程(baseloop)
    │ 用 Acceptor 监听新连接
    │     ↓ 新连接到来
    │ NextLoop() 选择一个子线程
    │     ↓
    ├──→ 子线程1 (EventLoop_1) 处理连接A, D, G...
    ├──→ 子线程2 (EventLoop_2) 处理连接B, E, H...
    └──→ 子线程3 (EventLoop_3) 处理连接C, F, I...
    
  轮询分配 → 负载均衡

3.2 LoopThreadPool类实现

cpp 复制代码
// ==================== EventLoop线程池 ====================
class LoopThreadPool {
private:
    int _thread_count;              // 线程池中有多少个子线程
    int _next_idx;                  // 下一个要分配的线程索引
    EventLoop *_baseloop;           // 主线程的 EventLoop
    std::vector<LoopThread*> _threads;  // 所有子线程
    std::vector<EventLoop*> _loops;     // 所有子线程的 EventLoop
    
public:
    // 构造函数
    LoopThreadPool(EventLoop *baseloop) 
        : _thread_count(0), _next_idx(0), _baseloop(baseloop)
    {
    }
    
    // 设置线程数量(创建之前调用)
    void SetThreadCount(int count) { 
        _thread_count = count; 
    }
    
    // 创建所有线程
    void Create() {
        if (_thread_count > 0) {
            // 预分配空间
            _threads.resize(_thread_count);
            _loops.resize(_thread_count);
            
            // 创建每一个线程
            for (int i = 0; i < _thread_count; i++) {
                _threads[i] = new LoopThread();      // 创建线程
                _loops[i] = _threads[i]->GetLoop();  // 获取它的 EventLoop
            }
        }
        return;
    }
    
    // 轮询获取下一个 EventLoop
    EventLoop *NextLoop() {
        if (_thread_count == 0) {
            // 没有子线程,就返回主线程的 EventLoop
            return _baseloop;
        }
        
        // 轮询:0 → 1 → 2 → 0 → 1 → 2 → ...
        _next_idx = (_next_idx + 1) % _thread_count;
        return _loops[_next_idx];
    }
};

NextLoop() 轮询逻辑:

bash 复制代码
初始:_next_idx = 0,_thread_count = 3

第1次调用:_next_idx = (0 + 1) % 3 = 1  → 返回 _loops[1]
第2次调用:_next_idx = (1 + 1) % 3 = 2  → 返回 _loops[2]
第3次调用:_next_idx = (2 + 1) % 3 = 0  → 返回 _loops[0]
第4次调用:_next_idx = (0 + 1) % 3 = 1  → 返回 _loops[1]
...

这样就实现了轮询分配,每个连接轮流分配给不同的线程

四、server.hpp 中类的顺序

因为各个类之间有依赖关系,顺序很重要。正确的顺序是:

cpp 复制代码
// 1. 所有 #include 和日志宏
#include <iostream>
// ... 其他头文件 ...
#define LOG(...)
// ... 其他宏 ...

// 2. Buffer 类
class Buffer { ... };

// 3. NetWork 和 Socket 类
class NetWork { ... };
class Socket { ... };

// 4. 前向声明(告诉编译器这些类存在)
class Poller;
class EventLoop;

// 5. Channel 类
class Channel { ... };  // 里面 Update/Remove 只声明

// 6. Poller 类
class Poller { ... };

// 7. TimerTask 类
class TimerTask { ... };

// 8. TimerWheel 类(前向声明 EventLoop)
class TimerWheel { ... };  // 对外接口只声明

// 9. EventLoop 类
class EventLoop { ... };

// 10. 在这里实现之前只声明的方法
void Channel::Update() { _loop->UpdateEvent(this); }
void Channel::Remove() { _loop->RemoveEvent(this); }

inline void TimerWheel::TimerAdd(...) { ... }
inline void TimerWheel::TimerRefresh(...) { ... }
inline void TimerWheel::TimerCancel(...) { ... }

// 11. LoopThread 类
class LoopThread { ... };

// 12. LoopThreadPool 类
class LoopThreadPool { ... };

// 13. #endif
#endif

五、测试TimerWheel + EventLoop

5.1 定时器测试代码

创建测试文件:

bash 复制代码
cd ~/TcpServer/test
vim timer_eventloop_test.cpp
cpp 复制代码
#include "../source/server.hpp"

int main() {
    EventLoop loop;
    
    DBG_LOG("添加定时任务:3秒后执行");
    loop.TimerAdd(1, 3, [](){
        DBG_LOG("定时任务1被执行了!3秒到了!");
    });
    
    DBG_LOG("添加定时任务:5秒后执行");
    loop.TimerAdd(2, 5, [](){
        DBG_LOG("定时任务2被执行了!5秒到了!");
    });
    
    DBG_LOG("进入事件循环,等待定时任务执行...");
    loop.Start();
    
    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++14 timer_eventloop_test.cpp -o timer_eventloop_test -lpthread
./timer_eventloop_test

预期输出:

bash 复制代码
[...] 添加定时任务:3秒后执行
[...] 添加定时任务:5秒后执行
[...] 进入事件循环,等待定时任务执行...
[...] 定时任务1被执行了!3秒到了!
[...] 定时任务2被执行了!5秒到了!

六、测试线程池

6.1 线程池测试代码

创建测试文件:

bash 复制代码
vim threadpool_test.cpp
cpp 复制代码
#include "../source/server.hpp"

int main() {
    EventLoop baseloop;
    
    // 创建线程池
    LoopThreadPool pool(&baseloop);
    pool.SetThreadCount(3);  // 设置3个子线程
    pool.Create();           // 创建线程
    
    DBG_LOG("线程池创建完毕,3个子线程已启动");
    
    // 验证轮询分配
    for (int i = 0; i < 6; i++) {
        EventLoop *loop = pool.NextLoop();
        DBG_LOG("第%d次获取EventLoop,地址=%p", i + 1, (void*)loop);
    }
    
    DBG_LOG("测试完成,程序退出");
    
    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++14 threadpool_test.cpp -o threadpool_test -lpthread
./threadpool_test

预期输出(地址不同线程每次都不同):

bash 复制代码
[...] 线程池创建完毕,3个子线程已启动
[...] 第1次获取EventLoop,地址=0x7f8a0c001000
[...] 第2次获取EventLoop,地址=0x7f8a0b800000
[...] 第3次获取EventLoop,地址=0x7f8a0b000000
[...] 第4次获取EventLoop,地址=0x7f8a0c001000  ← 回到第一个
[...] 第5次获取EventLoop,地址=0x7f8a0b800000
[...] 第6次获取EventLoop,地址=0x7f8a0b000000
[...] 测试完成,程序退出

轮询分配正常!


七、提交代码

bash 复制代码
cd ~/TcpServer
git add .
git commit -m "实现TimerWheel定时器、LoopThread、LoopThreadPool线程池"
git push

八、本篇总结

模块 功能
TimerTask 定时任务封装,析构时自动执行
TimerWheel 时间轮定时器,每秒tick一次
LoopThread 一个线程绑定一个EventLoop
LoopThreadPool 管理多个LoopThread,轮询分配

当前 server.hpp 结构:

cpp 复制代码
// 日志宏
// Buffer类
// NetWork + Socket类
// Channel类
// Poller类
// TimerTask + TimerWheel类
// EventLoop类
// Channel::Update/Remove 实现
// TimerWheel 对外接口实现
// LoopThread类
// LoopThreadPool类

关键概念总结:

  • 时间轮:时间复杂度 O(1) 的定时器,比二叉堆更高效
  • weak_ptr + shared_ptr:weak_ptr 观测对象是否还活着,避免循环引用
  • 条件变量:用于线程间的同步,等待某个条件成立
  • 轮询分配:循环轮流分配,保证各线程负载均衡

💬 下一篇预告:实现Connection模块和Acceptor模块,然后组装TcpServer!写完就能跑一个完整的Echo服务器了!


相关推荐
一只自律的鸡2 小时前
【Linux系统编程】信号 kill/raise/alarm/pause/alarm实例/漏桶算法
linux·运维·服务器
莫白媛2 小时前
Linux中Docker介绍与使用小白篇
linux·运维·docker
j_xxx404_2 小时前
蓝桥杯基础--模拟
数据结构·c++·算法·蓝桥杯·排序算法
ljh5746491192 小时前
linux xargs 命令
linux·运维·windows
sqyno1sky2 小时前
零成本抽象在C++中的应用
开发语言·c++·算法
xingyuzhisuan2 小时前
4090服务器内存怎么配?128GB起步还是256GB才够用?
运维·服务器
cm6543202 小时前
C++中的职责链模式
开发语言·c++·算法
夏语灬2 小时前
CST Studio Suite软件安装步骤(附安装包)CST Studio Suite 2024超详细下载安装教程
运维·服务器
zly35002 小时前
esxi后台 vcenter 进行身份验证过程中出错
运维·服务器