定时器应⽤
- ⼼跳检测
- 技能冷却
- 武器冷却
- 倒计时
- 其它需要使⽤超时机制的功能
适合定时器的数据结构有红黑树,最小堆,跳表,时间轮,其中最小堆实现的定时器最常见
最小堆的堆顶永远是最小的,超时检测时只需要从堆顶开始检测就行
时间获取与定时函数
由于是跨平台,所以不使用操作系统所提供的,
linux下可以使用timerfd,timerfd被抽象成fd文件,配合epoll_wait的timeout参数可以使心跳线程和工作线程处于同一线程,避免伪心跳包出现
c++提供三种获取时间的类
- std::chrono::system_clock
- std::chrono::steady_clock
- std::chrono::high_resolution_clock
由于system_clock是获取系统时间,如果系统时间被更改,或者网络校时,都会使时间被更改,所以不适合使用,steady_clock是只会自增(例如开机时间),high_resolution_clock在不同的系统中可能有不同的实现(通常它只是 std::chrono::steady_clock或 std::chrono::system_clock的别名),所以我们选择std::chrono::steady_clock
而定时则选取std::condition_variable::wait_until函数
定时器接口
最小堆实现
对于每一个任务都要有以下几个基本字段描述
- 超时后调用的回调函数
- 超时的时长
- 以及一个该任务是否一直执行的标志
对于每一个定时器有几个基本方法描述
- 初始化一个定时器
- 添加任务
- 执行到期任务
c++对象封装后的代码
timer.h
#pragma once
#include <functional>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>
using timedTesk = std::function<void()>;
using timeDuration = std::chrono::milliseconds;
using timePoint = std::chrono::time_point<std::chrono::steady_clock>;
class timerNode {
public:
bool operator < (const timerNode& obj) const {
return point > obj.getPoint(); }
timePoint getPoint() const {
return point; }
timerNode(int milliseconds, timedTesk fun, bool keep);
timerNode(const timerNode& obj);
void operator()() const {
tesk(); }
bool is_keep() const {
return keep; }
//刷新超时时间点
void flush();
private:
timedTesk tesk;//定时任务
bool keep;//是否保活
timeDuration time;//超时时长
timePoint point;//执行的时间点
};
class timer
{
public:
//添加任务
void addTimer(int time, timedTesk tesk, bool keep);
//执行定时器
void start();
//销毁定时器
void destroy();
private:
//执行到期任务:expire_timer
void expire_timer();
private:
//存放定时任务的小根堆
std::priority_queue<timerNode, std::vector<timerNode>, std::less<timerNode>> queue;
//保证小根堆线程安全
std::mutex que_mtx;
//定时器销毁标志
bool is_destroy;
//定时std::condition_variable::wait_until
std::mutex mtx;
std::condition_variable cv;
};
timer.cpp
#include "timer.h"
#include <climits>
#include <thread>
timerNode::timerNode(int milliseconds, timedTesk fun, bool keep)
:time(milliseconds), tesk(fun), keep(keep)
{
flush();
}
timerNode::timerNode(const timerNode& obj) :time(obj.time), tesk(obj.tesk), keep(obj.keep)
{
flush();
}
void timerNode::flush()
{
point = std::chrono::steady_clock::now() + time;
}
void timer::addTimer(int time, timedTesk tesk, bool keep)
{
std::lock_guard<std::mutex> lock(que_mtx);
queue.emplace(time, tesk, keep);
cv.notify_one();
}
void timer::start()
{
is_destroy = false;
std::thread th(&timer::expire_timer, this);
th.detach();
}
void timer::destroy()
{
is_destroy = true;
}
void timer::expire_timer()
{
while (!is_destroy)
{
std::unique_lock<std::mutex> lock(mtx);
//获取堆顶时间,没有则wait一个最大时间
auto time = queue.empty() ? std::chrono::steady_clock::now() + std::chrono::hours(INT_MAX) : queue.top().getPoint();
if (cv.wait_until(lock, time) == std::cv_status::no_timeout) {
//有新的定时任务加入
//新任务中可能存在比之前超时时间更小的任务,通过continue刷新超时时间
continue;
}
else {
//处理超时任务
while (!queue.empty() && queue.top().getPoint() < std::chrono::steady_clock::now())
{
timerNode node = queue.top();
{
std::lock_guard<std::mutex> lock_(que_mtx);
queue.pop();
/*
如果tesk设置了keep为true,但是直接修改超时的时间点的话tesk对象在小根堆中的位置不会改变,所以只能通过先pop在push的方式更新tesk节点在小根堆中的位置
*/
if (node.is_keep())
queue.emplace(node);
}
node();//执行tesk运算符重载函数
}
}
}
}