前面我们已经对这个项目进行了基本的模块分析拆解,本篇我们在开始前先了解一些前置知识并开始进行项目编写
相关的代码如下:仿muduo服务器: 本项目致力于实现一个仿造muduo库的简易并发服务器,为个人项目,参考即可
目录
[read 读取到期次数(非专用接口,但常用)](#read 读取到期次数(非专用接口,但常用))
前置知识
bind
cpp
/*
std::bind 是 C++11 引入的函数适配器,位于 <functional> 中。它将可调用对象与其部分参数绑定,
生成一个新的可调用对象,支持参数占位符(_1, _2, ...)实现参数重排、延迟求值及部分应用
它的简化表达式为:
namespace std
{
template< class F, class... BoundArgs >
auto bind( F&& f, BoundArgs&&... bound_args );
}
其中 F 是可调用对象类型,BoundArgs 是要绑定的参数类型。bind 返回一个新的可调用对象,
当调用该对象时,会将绑定的参数与占位符参数一起传递
std::placeholders::_1, _2, ... 是占位符,用于指定调用时传入的参数位置。例如:
auto f = std::bind(func, _1, 42); // 绑定 func
*/
#include <iostream>
#include <functional>
void add(int a, int b) { std::cout << a + b << '\n'; }
struct Mul
{
int operator()(int a, int b) const { return a * b; }
int member(int x, int y) const { return x - y; }
};
int main()
{
using namespace std::placeholders; // _1, _2...
// 1. 固定第一个参数为 10,第二个由调用时指定
auto add10 = std::bind(add, 10, _1);
add10(5); // 输出 15
// 2. 参数重排:将 (a,b) -> (b,a)
auto rev = std::bind(add, _2, _1);
rev(3, 7); // 输出 10 (add(7,3))
// 3. 绑定函数对象
Mul mul;
auto product = std::bind(mul, _1, _2);
std::cout << product(4, 5) << '\n'; // 20
// 4. 绑定成员函数:需传对象指针或引用
auto sub = std::bind(&Mul::member, &mul, _1, _2);
std::cout << sub(10, 3) << '\n'; // 7
// 5. 结合 lambda(bind 通常不如 lambda 直观,但可统一旧接口)
auto lam = std::bind([](int x, int y) { return x * y; }, 6, _1);
std::cout << lam(7) << '\n'; // 42
}
timerfd
timerfd 是Linux 特有 的定时器接口,它将定时器事件以文件描述符的形式呈现,使得定时器可以像普通文件一样被 read、select、poll、epoll 等 I/O 多路复用机制管理
核心特性
-
创建 :
timerfd_create(clockid_t clockid, int flags)返回一个文件描述符,
clockid可选CLOCK_REALTIME、CLOCK_MONOTONIC等,flags可设TFD_NONBLOCK、TFD_CLOEXEC。 -
启动/设置 :
timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value)定义首次超时时间和后续间隔周期,
flags=0表示绝对时间,TFD_TIMER_ABSTIME表示相对时间。 -
读取事件 :当定时器到期,
read(fd, buf, sizeof(uint64_t))会返回一个uint64_t整数,表示自上次读取后到期的次数(通常用于处理多次到期合并)。 -
关闭 :
close(fd)。
优势
-
统一事件源 :将定时器与 socket、信号等一起放入
epoll循环,无需单独维护定时器列表。 -
精确到期计数:可避免信号式定时器的异步安全问题。
相关接口
timerfd_create
-
作用
创建一个定时器对象,并将其与一个新的文件描述符关联。此后该定时器可通过文件描述符进行 I/O 监控(如
poll、epoll),到期事件通过read读取。 -
表达式
cppint timerfd_create(int clockid, int flags); -
参数
-
clockid:指定定时器使用的时钟源。常用值:CLOCK_REALTIME(系统实时时钟,受系统时间调整影响)、CLOCK_MONOTONIC(单调递增时钟,不受系统时间变更影响)。 -
flags:修改行为。TFD_NONBLOCK使描述符为非阻塞模式;TFD_CLOEXEC在执行exec时自动关闭描述符。
-
-
返回值
成功时返回一个非负整数文件描述符;失败时返回
-1,并设置errno指示错误原因。
timerfd_settime
-
作用
启动或停止由
timerfd_create创建的定时器,设置首次到期时间以及后续周期性间隔。 -
表达式
cppint timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value); -
参数
-
fd:timerfd_create返回的文件描述符。 -
flags:控制时间的解释方式。0表示相对时间(new_value中的时间相对于调用时刻);TFD_TIMER_ABSTIME表示绝对时间(基于时钟的绝对点)。 -
new_value:指向itimerspec结构的指针,包含两个成员:it_value(首次到期时间)和it_interval(后续周期间隔)。若it_value为零则停止定时器;若it_interval为零则为单次定时器。 -
old_value:非空时,返回定时器之前设置的剩余时间及间隔;可为NULL表示不关心。
-
-
返回值
成功返回
0;失败返回-1,并设置errno。
timerfd_gettime
-
作用
查询定时器的当前状态,即距离下一次到期的剩余时间以及当前的周期间隔。
-
表达式
int timerfd_gettime(int fd, struct itimerspec *curr_value); -
参数
-
fd:定时器文件描述符。 -
curr_value:输出参数。调用成功后,curr_value->it_value返回距离下一次到期的剩余时间(若定时器已停止则为零),curr_value->it_interval返回定时器的周期间隔(与设置时相同)。
-
-
返回值
成功返回
0;失败返回-1,并设置errno。
read 读取到期次数(非专用接口,但常用)
-
作用
从定时器描述符中读取到期次数。每次定时器到期,内核会增加一个内部计数器;
read会取出该计数器的当前值并清零。这允许程序在一次read中获知自上次读取以来到期的总次数,适用于处理积压事件。 -
表达式
cppssize_t read(int fd, void *buf, size_t count); -
参数
-
fd:timerfd_create返回的文件描述符。 -
buf:必须指向一个uint64_t类型的变量,用于接收到期计数。 -
count:必须为sizeof(uint64_t)(即 8 字节),否则read可能失败或读取不完整。
-
-
返回值
成功时返回读取的字节数(通常为 8)。若定时器从未到期且描述符为非阻塞模式,
read会立即返回-1并设置errno为EAGAIN或EWOULDBLOCK;若描述符为阻塞模式,则一直阻塞直到至少有一次到期。失败时返回-1并设置errno。
示例
cpp
/*
总体介绍:
timerfd是Linux下特有的时间轮定时器,
提供了一个文件描述符接口,可以通过read()系统调用来获取定时器的到期次数。
它支持单次定时和周期性定时,可以设置初始延迟和间隔时间。
timerfd的使用可以简化定时器的管理,避免了传统定时器中需要处理信号或线程同步的问题。
常用结构体成员介绍:
struct itimerspec
{
struct timespec it_interval; // 周期间隔
struct timespec it_value; // 首次到期时间
};
struct timespec
{
time_t tv_sec; // 秒
long tv_nsec; // 纳秒(范围 0 ~ 999999999)
};
成员详解:
it_value:指定定时器的首次到期时间。
若 it_value.tv_sec 和 it_value.tv_nsec 均非零,定时器在该绝对/相对时间点首次触发。
若两者均为零,定时器被停止( disarm)。
若使用相对时间模式(flags=0),it_value 表示从调用时刻开始的时长。
若使用绝对时间模式(TFD_TIMER_ABSTIME),it_value 表示基于时钟的绝对时间点。
it_interval:指定定时器的周期间隔。
若 it_interval 的任一成员非零,定时器在首次到期后,每隔该间隔重复触发。
若 it_interval.tv_sec 和 it_interval.tv_nsec 均为零,定时器为单次模式,首次到期后即停止。
使用注意:
tv_nsec 的值必须小于 1e9(即 1 秒)。当设置绝对时间时,需注意 it_value 不能小于当前时间,否则可能立即到期或导致错误。
*/
#include <sys/timerfd.h>
#include <unistd.h>
#include <stdint.h>
#include <iostream>
#include <cstring>
#include <cerrno>
int main()
{
// 1. timerfd_create:创建定时器描述符
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
if (tfd == -1)
{
std::cerr << "timerfd_create failed: " << strerror(errno) << std::endl;
return 1;
}
// 配置定时器:首次 1 秒后到期,之后每隔 1 秒周期触发
struct itimerspec new_value = {};
new_value.it_value.tv_sec = 1; // 首次到期时间:1 秒后
new_value.it_value.tv_nsec = 0;
new_value.it_interval.tv_sec = 1; // 周期间隔:1 秒
new_value.it_interval.tv_nsec = 0;
// 2. timerfd_settime:启动定时器
if (timerfd_settime(tfd, 0, &new_value, nullptr) == -1)
{
std::cerr << "timerfd_settime failed: " << strerror(errno) << std::endl;
close(tfd);
return 1;
}
// 3. timerfd_gettime:获取定时器当前剩余时间和间隔
struct itimerspec curr = {};
if (timerfd_gettime(tfd, &curr) == -1)
{
std::cerr << "timerfd_gettime failed: " << strerror(errno) << std::endl;
}
else
{
std::cout << "Initial remaining time: " << curr.it_value.tv_sec
<< " sec " << curr.it_value.tv_nsec << " ns" << std::endl;
std::cout << "Interval: " << curr.it_interval.tv_sec
<< " sec " << curr.it_interval.tv_nsec << " ns" << std::endl;
}
// 循环读取 5 次到期事件
for (int i = 0; i < 5; ++i)
{
uint64_t exp = 0;
// 4. read:阻塞读取到期次数
ssize_t s = read(tfd, &exp, sizeof(exp));
if (s != sizeof(exp))
{
std::cerr << "read error: " << strerror(errno) << std::endl;
break;
}
std::cout << "Timer expired " << exp << " time(s)" << std::endl;
// 再次调用 timerfd_gettime 查看剩余时间
if (timerfd_gettime(tfd, &curr) == 0)
{
std::cout << " Next expiration in: " << curr.it_value.tv_sec
<< " sec " << curr.it_value.tv_nsec << " ns" << std::endl;
}
}
// 5. close:关闭描述符,释放定时器
close(tfd);
return 0;
}
结果为:

时间轮定时器
在上述的代码中,存在⼀个很大的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率无疑是较为低下的。
或许我们可以用优先级队列建立一个从大到小排序的堆,时间小的逐个释放,这样也是一个不错的思路。
这里我们介绍另一种思路:时间轮
思路讲解:如果我们定义了一个数组,并且有一个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定一个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。
但是有以下几个情况需要解决:
- 同一时刻的定时任务只能添加一个,需要考虑如何在同一时刻支持添加多个定时任务
解决方案:将时间轮的一维数组设计为二维数组(时间轮一位数组的每一个节点也是一个数组)
- 假设当前的定时任务是一个连接的非活跃销毁任务,这个任务什么时候添加到时间轮中比较合适?
一个连接30s内都没有通信,则是一个非活跃连接,这时候就销毁
但是一个连接如果在建立的时候添加了一个30s后销毁的任务,但是这个连接30s内人家有数据通信,在第30s的时候不是一个非活跃连接。
思想 :需要在一个连接有IO事件产生的时候,延迟定时任务的执行
作为一个时间轮定时器,本身并不关注任务类型,只要是时间到了就需要被执行。
解决方案:类的析构函数 + 智能指针shared_ptr,通过这两个技术可以实现定时任务的延时

源码实现
timewheel.hpp
cpp
#pragma once
#include <memory>
#include <cstdint>
#include <vector>
#include <functional>
#include <unordered_map>
namespace IMMUDUO
{
using Task=std::function<void()>;
using Release=std::function<void()>;
class TimeTask
{
public:
TimeTask(uint64_t id,uint32_t timeout,const Task& task);
~TimeTask();
//设置定时器任务对象被销毁时需要执行的任务
void SetRelease(const Release& release);
//获取定时器任务对象超时时间
uint32_t GetTimeout()const;
//获取定时器任务对象ID
uint64_t GetId()const;
private:
uint64_t id_; //定时器任务对象ID
uint32_t timeout_; //定时器任务超时时间
Task task_; //定时器需要执行的任务
Release release_; //定时器任务对象被销毁时需要执行的任务
};
class TimeWheel
{
using SharedTask=std::shared_ptr<TimeTask>;
using WeakTask=std::weak_ptr<TimeTask>;
void RemoveTimer(uint64_t id);
public:
TimeWheel();
//添加定时器任务对象到时间轮中
void TimerAdd(uint64_t id,uint32_t timeout,const Task& task);
//刷新定时任务
void TimerRefresh(uint64_t id);
//执行定时任务
void TimerRunTask();
~TimeWheel()=default;
private:
int ticks_; //当前秒针,走到哪里执行哪里任务
int capacity_; //最大延迟时间
std::vector<std::vector<SharedTask>>wheel_; //时间轮,包含多个槽,每个槽中包含多个定时器任务对象
std::unordered_map<uint64_t,WeakTask> taskMap_; //定时器任务对象ID与定时器任务对象的映射关系
};
}
timewheel.cpp
cpp
#include "timewheel.hpp"
namespace IMMUDUO
{
TimeTask::TimeTask(uint64_t id,uint32_t timeout,const Task& task)
:id_(id),timeout_(timeout),task_(task)
{}
TimeTask::~TimeTask()
{
task_();
release_();
}
void TimeTask::SetRelease(const Release& release)
{
release_=release;
}
uint32_t TimeTask::GetTimeout()const
{
return timeout_;
}
uint64_t TimeTask::GetId()const
{
return id_;
}
TimeWheel::TimeWheel()//初始化列表顺序取决于类内成员声明的顺序
:ticks_(0),capacity_(60),wheel_(capacity_)
{}
void TimeWheel::RemoveTimer(uint64_t id)
{
if(taskMap_.find(id)!=taskMap_.end())
{
taskMap_.erase(id);
}
}
void TimeWheel::TimerAdd(uint64_t id,uint32_t timeout,const Task& task)
{
SharedTask taskPtr=std::make_shared<TimeTask>(id,timeout,task);
//bind写法
// taskPtr->SetRelease(std::bind(&TimeWheel::RemoveTimer,this,id));
//推荐lambda表达式
taskPtr->SetRelease([this,id](){
this->RemoveTimer(id);
});
taskMap_[id]=WeakTask(taskPtr);
int pos=(ticks_+timeout)%capacity_;
wheel_[pos].push_back(taskPtr);
}
void TimeWheel::TimerRefresh(uint64_t id)
{
auto it=taskMap_.find(id);
//通过定时器的weak_ptr获取shared_ptr的定时器任务对象
if(it==taskMap_.end())
{
return ;//没找到刷新任务,无法刷新,无法延迟
}
SharedTask taskPtr=it->second.lock();//获取对应的shared_ptr的定时器任务对象
if(!taskPtr)
{
return ;//weak_ptr已经失效,无法刷新
}
int dlay=taskPtr->GetTimeout();
int pos=(ticks_+dlay)%capacity_;
wheel_[pos].push_back(taskPtr);
}
void TimeWheel::TimerRunTask()
{
ticks_=(ticks_+1)%capacity_;
wheel_[ticks_].clear();//走到哪里释放哪里的任务对象
}
}
本期内容到这里就结束了,喜欢请点个赞谢谢
封面图自取:
