服务器程序通常管理着众多定时事件,比如定期检测一个客户连接的活动状态,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。
为此,程序员通常将每个定时事件分别封装成定时器,并使用某种容器类数据结构(比如链表、 排序链表和时间轮),将所有定时器串联起来,以实现对定时事件的统一管理。
设计定时器的一种思路是将所有定时事件放入一个排序容器,主循环以固定的频率调用一个心搏函数tick(),在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。
设计定时器的另一种思路是:使用时间堆,将所有定时事件放在一个最小堆中,将最小堆中的堆顶元素的到期时间最近的时间间隔作为心搏间隔。这样,一旦心搏函数tick()被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。
1、计时器的数据结构
#include <chrono>
typedef std::function<void()> TimeoutCallBack;// 返回值为空的对象,定时回调
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::milliseconds MS;// 毫秒级别
typedef Clock::time_point TimeStamp;// 时间点,单位秒数
struct TimerNode {
int id;// 标识符,该项目中是socket描述符
TimeStamp expires;// 到期时间点,使用的自1970年开始的绝对时间,单位:秒
TimeoutCallBack cb;// 定时回调函数,到期时应该执行的操作
// < 运算符重载函数,用来比较不同的时间点,方便排序
bool operator<(const TimerNode& t) {
return expires < t.expires;
}
};
std::vector<TimerNode> heap_;// 时间堆(最小堆,到期时间最小的在第1个)
std::unordered_map<int, size_t> ref_;// 哈希表<TimerNode.id,>
2、计时器的初始化
HeapTimer() { heap_.reserve(64);// 初始化容量为64 }
3、执行定时事件
int HeapTimer::GetNextTick() {
tick();// 跳动函数,执行所有到期的定时事件
size_t res = -1;
// 如果堆不为空
if(!heap_.empty()) {
// 计算堆顶元素的到期间隔
res = std::chrono::duration_cast<MS>(heap_.front().expires - Clock::now()).count();
if(res<0) {res=0;}
}
return res;
}
// 跳动函数(执行定时事件)
void HeapTimer::tick() {
if(heap_.empty()) {
return;
}
// 遍历堆,将所有到期的定时事件全部执行
while(!heap_.empty()) {
TimerNode node = heap_.front();//取第一个节点
// 如果还没有到期,跳出循环
if(std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) {
break;
}
node.cb();// 调用回调函数
pop();// 去除该节点
}
}
// 删除最后一个节点
void HeapTimer::pop() {
assert(!heap_.empty());
del_(0);
}
void HeapTimer::del_(size_t index) {
/* 删除指定位置的结点 */
assert(!heap_.empty() && index >= 0 && index < heap_.size());
/* 将要删除的结点换到队尾,然后调整堆 */
size_t i = index;
size_t n = heap_.size() - 1;
assert(i <= n);
if(i < n) {
SwapNode_(i, n);
if(!siftdown_(i, n)) {
siftup_(i);
}
}
/* 队尾元素删除 */
ref_.erase(heap_.back().id);
heap_.pop_back();
}
4、添加定时事件
添加一个定时事件
m_pTimer->add(fd, timeoutMS_, bind(&WebServer::CloseConn_, this, &m_UsersDict[fd]));
// 添加一个定时事件,参数分别是socket描述符、时间间隔、回调函数
void HeapTimer::add(int id, int timeout, const TimeoutCallBack& cb) {
assert(id >= 0);
size_t i;
// 如果ref_中不存在id,队尾添加新节点,调整堆
if(ref_.count(id) == 0) {
i = heap_.size();// 大小
ref_[id] = i;
heap_.push_back({id, Clock::now() + MS(timeout), cb});// 添加一个TimerNode
siftup_(i);
}
// 如果ref_中不存在id,覆盖原有节点,调整堆
else {
i = ref_[id];
heap_[i].expires = Clock::now() + MS(timeout);
heap_[i].cb = cb;
if(!siftdown_(i, heap_.size())) {
siftup_(i);
}
}
}
调整某个定时事件:m_pTimer->adjust(client->GetFd(), timeoutMS_);
void HeapTimer::adjust(int id, int timeout) {
/* 调整指定id的结点 */
assert(!heap_.empty() && ref_.count(id) > 0);
heap_[ref_[id]].expires = Clock::now() + MS(timeout);;
siftdown_(ref_[id], heap_.size());
}
5、调整堆结构
void HeapTimer::SwapNode_(size_t i, size_t j) {
assert(i >= 0 && i < heap_.size());
assert(j >= 0 && j < heap_.size());
std::swap(heap_[i], heap_[j]);
ref_[heap_[i].id] = i;
ref_[heap_[j].id] = j;
}
void HeapTimer::siftup_(size_t i) {
assert(i >= 0 && i < heap_.size());// 检查参数是否合法
size_t j = (i-1) / 2;
while(j >= 0) {
if(heap_[j] < heap_[i]) { break; }
SwapNode_(i, j);
i=j;// 更新i
j=(i-1)/2;// 更新j
}
}
bool HeapTimer::siftdown_(size_t index, size_t n) {
assert(index >= 0 && index < heap_.size());// 检查参数是否合法
assert(n >= 0 && n <= heap_.size());// 检查参数是否合法
size_t i = index;
size_t j = i * 2 + 1;
while(j < n) {
if(j + 1 < n && heap_[j + 1] < heap_[j]) j++;
if(heap_[i] < heap_[j]) break;
SwapNode_(i, j);// 交换两个节点
i = j;
j = i * 2 + 1;
}
return i > index;
}