一个基于优先队列的C++定时器

定时器

定时器的实现方式有多种,底层实现有双向链表,最小堆实现以及时间轮。

本文以基于最小堆实现一个简单的定时器。使用了C++ 11中实现了priority_queue模板,虽然名字中带有queue,但是实现方式是堆,名字中带有queue,只是因为使用queue的接口。

底层原理

链表

链表是一种简单的数据结构。只需要一条升序的链表即可实现一个简单的定时器。每一次有新的定时器加入,则从链表头开始遍历,直到遇到可插入的位置。

在时间复杂度上,使用链表编写的定时器插入和删除的时间复杂度皆为O(n)。

最小堆

最小堆在c++ 11 中提供了std::priority_queue模板。std::priority_queue默认构造为最大堆。如果需要使用最小堆,则需在构造时添加第三个参数,一个仿函数。

例如:我需要利用std::priority_queue实现升序Timer。

using TimePoint = std::chrono::high_resolution_clock::time_point;
Timer{
public:
	TimePoint getExpireTime() const { return expireTime_; };
private:
	TimePoint expireTime_;
}
struct TimerCmp {
	bool operator()(Timer *a, Timer *b) {
		return (a->getExpireTime() > b->getExpireTime());
	}
};
std::priority_queue <Timer *, std::vector<Timer *>,TimerCmp> mangerQueue_; 

在插入和删除上,堆的时间复杂度皆为O(logN)

时间轮

时间轮是一种稍微复杂点的数据结构。有简单单层时间轮,也有分层时间轮。
单层时间轮较为简单就不展开讨论了。这里说一说分层时间轮。分层时间轮每一个粒度对应一个时间轮,由多个时间轮进行度调。解决了单层时间轮刻度过大造成的空间浪费以及刻度过小时任务列表过长的问题。

时间轮实现了在插入和删除操作中时间复杂度皆为O(1)。

场景

在任务量小的场景下:最小堆实现,可以根据堆顶设置超时时间,数组存储结构,节省内存消耗,使用最小堆可以得到比较好的效果。而时间轮定时器较为复杂点,由于需要维护一个线程用来拨动指针,且需要开辟一个bucket数组,消耗内存大,使用时间轮会较为浪费资源。在任务量大的场景下:最小堆的插入复杂度是O(logN), 相比时间轮O(1) 会造成性能下降。更适合使用时间轮实现。

Chrono库

Durations

durations表示一个时间段。不过chrono库中已经封装好常用的时间单位,比如hours,miniutes,seconds,milliseconds等,最高精度可到纳秒。

Time Point

time_poitnt顾名思义,表示一个时间点。常用函数有

        now()  - 获取当前时间

Clocks

chrono库提供了三个时钟,分别为

        system_clock - 系统提供的实时时钟,精度为微秒。
        high_resolution_clock - 当前系统时钟周期最短的时钟,精度为纳秒。
        steady_clock - 不会被调整的单调时钟,精度为纳秒。

为了便于阅读,我使用using建立了别名。

using TimeOutFuction = std::function<void()>;
using MS = std::chrono::milliseconds;
using Clock = std::chrono::high_resolution_clock;
using TimePoint = Clock::time_point;

Timer

Timer
-TimePoit expireTime_
-bool used_
-TimeOutFuction timeOutFuction_
+void setUsed(bool used)
+bool isUsed()
+TimePoint getExpiredTime()
+void runTimeOutFunction()

TimerManager

TimerManager
-priority_queuq,TimerCmp> mangerQueue_
-TimePoint nowTime_
-mutex lock_
+void updateTime()
+Timer* addTimer()
+void delTimer()
+void tick()
+int getExpireTime()

其中最初设计时优先队列并非存贮Timer指针,而是直接存储对象。但是在addTimer函数中发现一个问题。直接使用TImer的话。addTimer函数返回值则是返回新生成Timer对象的引用。但是在多线程操作中可能会使引用的内容提前被释放,导致程序错误。

实现

这个定时器我打算使用到自己的简易http服务器。tiny_http_server
Timer.h头文件

//Timer.h//
#pragma once
#include<chrono>
#include<queue>
#include<functional>
#include<mutex>
#include<memory>
using TimeOutFuction = std::function<void()>;
using MS = std::chrono::milliseconds;
using Clock = std::chrono::high_resolution_clock;
using TimePoint = Clock::time_point;

class Timer {
public:
	Timer(const TimePoint &t , TimeOutFuction timeOutFun) : expireTime_(t), timeOutFuction_(timeOutFun),
			used_(true){};
	void setUsed(bool used) { used_ = used; };
	bool isUsed() const { return used_; };
	void runTimeOutFunction() const { timeOutFuction_(); };
	TimePoint getExpireTime() const { return expireTime_; };
	bool operator<(const Timer& a) {
		return (expireTime_ < a.getExpireTime());
	}
private:
	TimePoint expireTime_;
	TimeOutFuction timeOutFuction_;
	bool used_;  
};
struct TimerCmp {
	bool operator()(std::shared_ptr<Timer> a, std::shared_ptr<Timer> b) {
		return (a->getExpireTime() > b->getExpireTime());
	}
};
class TimerManager {
public:
	TimerManager() :nowTime_(Clock::now()) {};
	~TimerManager() = default;
	void updateTime() { nowTime_ = Clock::now(); };
	std::shared_ptr<Timer> addTimer(const int &time , TimeOutFuction timeOutFun);   //返回引用 还是指针 是个问题
	void delTimer(std::shared_ptr<Timer> timer); //因为优先队列只能删除顶部,使用惰性删除,减少开销,真正删除在tick()和getExpireTime()
	void tick();		//心跳函数
	int getExpireTime();  //获取超时时间


private:	
	std::priority_queue <std::shared_ptr<Timer>, std::vector<std::shared_ptr<Timer>>,TimerCmp> mangerQueue_;  //Timer重载<,生成最小堆
	TimePoint nowTime_;
	std::mutex lock_;		
};


timer.cpp

timer.cpp
#include"timer.h"
std::shared_ptr<Timer> TimerManager::addTimer(const int& time, TimeOutFuction timeOutFun) {
	std::shared_ptr<Timer> timer = std::make_shared<Timer>(nowTime_ + MS(time), timeOutFun);
	{
		std::unique_lock<std::mutex> lock(lock_);
		updateTime();
		mangerQueue_.push(timer);
	}
	return timer;
}
void TimerManager::delTimer(std::shared_ptr<Timer> timer) {
	if (timer == nullptr) {
		return;
	}
	{
		//std::unique_lock<std::mutex> lock(lock_);	//会产生死锁bug,原因是在Timer的回调函数中调用此函数,会导致单线程重复加锁
		timer->setUsed(false);
	}
}

void TimerManager::tick() {
	{
		std::unique_lock<std::mutex> lock(lock_);
		updateTime();
		if (mangerQueue_.empty())
			return;
		while (!mangerQueue_.empty()) {
			std::shared_ptr<Timer> delTimer = mangerQueue_.top();
			if (!mangerQueue_.top()->isUsed()) {
				mangerQueue_.pop();
				continue;
			}
			if (std::chrono::duration_cast<MS>(mangerQueue_.top()->getExpireTime() - nowTime_).count() > 0)   //没有超时
				return;

			mangerQueue_.top()->runTimeOutFunction();
			mangerQueue_.pop();
		}
	}
}
int TimerManager::getExpireTime() {
	{
		std::unique_lock<std::mutex> lock(lock_);
		updateTime();
		int expireTime = 0;
		while (!mangerQueue_.empty() && !mangerQueue_.top()->isUsed()) {
			std::shared_ptr<Timer> delTimer = mangerQueue_.top();
			mangerQueue_.pop();
		}
		if (mangerQueue_.empty())
			return expireTime;

		expireTime = std::chrono::duration_cast<MS>(mangerQueue_.top()->getExpireTime() - nowTime_).count();
		expireTime < 0 ? 0 : expireTime;
		return expireTime;

	}
}


一个BUG

void TimerManager::delTimer(std::shared_ptr<Timer> timer) {
	if (timer == nullptr) {
		return;
	}
	{
		std::unique_lock<std::mutex> lock(lock_);	
		//会产生死锁bug,原因是在Timer的回调函数中调用此函数,会导致单线程重复加锁
		timer->setUsed(false);
	}
}

以前我在做这部分适合考虑不够周全,没有完全考虑到应用场景,简单来说就是在delTimer函数中加锁,会导致死锁,原因是调用这在Timer的回调函数中调用delTimer函数,导致void TimerManager::tick()函数会重复加锁

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值