文章目录
-
- 从零手写高并发服务器(七):定时器TimerWheel与线程池
- 一、TimerWheel定时器集成到EventLoop
-
- [1.1 回顾时间轮原理](#1.1 回顾时间轮原理)
- [1.2 需要的头文件](#1.2 需要的头文件)
- [1.3 TimerTask类------定时任务的封装](#1.3 TimerTask类——定时任务的封装)
- [1.4 TimerWheel类------时间轮实现](#1.4 TimerWheel类——时间轮实现)
- [1.5 TimerWheel对外接口的实现](#1.5 TimerWheel对外接口的实现)
- [1.6 在EventLoop中集成TimerWheel](#1.6 在EventLoop中集成TimerWheel)
- 二、EventLoopThread------线程与事件循环的绑定
-
- [2.1 为什么需要EventLoopThread?](#2.1 为什么需要EventLoopThread?)
- [2.2 LoopThread类实现](#2.2 LoopThread类实现)
- 三、EventLoopThreadPool线程池
-
- [3.1 线程池的作用](#3.1 线程池的作用)
- [3.2 LoopThreadPool类实现](#3.2 LoopThreadPool类实现)
- [四、server.hpp 中类的顺序](#四、server.hpp 中类的顺序)
- [五、测试TimerWheel + EventLoop](#五、测试TimerWheel + EventLoop)
-
- [5.1 定时器测试代码](#5.1 定时器测试代码)
- 六、测试线程池
-
- [6.1 线程池测试代码](#6.1 线程池测试代码)
- 七、提交代码
- 八、本篇总结
从零手写高并发服务器(七):定时器TimerWheel与线程池
💬 开篇:上一篇我们实现了EventLoop,但它还缺两个重要能力:定时任务管理和多线程支持。本篇我们把前置知识中学过的时间轮集成到EventLoop中,然后实现EventLoopThread和EventLoopThreadPool,让服务器具备多线程处理能力。
👍 点赞、收藏与分享:定时器用于连接超时管理,线程池用于并发处理,都是高并发服务器的核心组件。
🚀 循序渐进:TimerWheel集成 → EventLoopThread → EventLoopThreadPool → 测试验证。
一、TimerWheel定时器集成到EventLoop
1.1 回顾时间轮原理
前置知识篇我们已经实现过时间轮了,这里要做的是:
- 把TimerWheel集成到EventLoop中
- 用
timerfd驱动时间轮的tick(每秒触发一次) - 所有定时器操作都必须在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, ×, 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服务器了!