一个基于优先队列的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()函数会重复加锁

### 回答1: 可以先将定时器框架的功能和结构进行抽象,然后根据抽象的功能结构来实现对C语言的实现。可以采用C语言的多线程编程来实现定时器框架,如使用POSIX线程和timer_create()函数来实现定时器框架,并使用sigaction()函数来处理定时器到期时的信号。 ### 回答2: 设计一个C定时器框架需要考虑以下几个方面: 1. 定时器数据结构:可以采用链表或优先队列来存储定时器事件,每个结点包含定时时间、回调函数和参数等信息。 2. 定时器的接口:提供初始化、启动、停止、添加事件、删除事件等基本操作的接口函数。 3. 定时器的调度机制:可以使用一个循环结构不断检查定时器事件的触发情况,一旦达到定时时间,就执行相应的回调函数。 4. 定时器的精度和性能:可以通过选择合适的时钟精度来控制定时器触发的准确性和性能开销。例如,使用系统时钟提供的高精度定时器API,或者通过设置时间片大小来调节。 5. 多线程应用和线程安全:在多线程应用中,需要考虑线程同步和互斥机制,确保并发操作不会导致数据竞争或死锁等问题。 6. 容错和异常处理:设计定时器框架时需要考虑异常情况的处理方式,例如定时器触发出错、回调函数异常、时钟漂移等问题,可以采用适当的错误处理机制和日志记录。 总之,设计一个C定时器框架需要充分考虑功能需求、性能需求和安全需求,合理选择数据结构和算法,并进行充分的测试和优化,以确保高效且可靠地实现定时器功能。 ### 回答3: 设计一个C定时器框架可以帮助我们实现定时操作,并且提高程序的效率。以下是一种设计思路。 首先,我们需要定义一个定时器结构体,其中包含一个时间戳变量用于记录定时操作的时间点,一个函数指针用于指向定时器触发时需要执行的函数,以及其他可选的自定义参数。 接下来,我们需要实现一些基本的功能函数。首先是初始化函数,用于初始化定时器框架的内部变量和数据结构。然后是一个定时注册函数,用于注册一个定时器。该函数需要接受一个时间间隔参数,并将定时器结构体添加到定时器队列中。我们还可以实现一个取消注册函数,用于取消已注册的定时器。这样,我们就可以动态地添加和删除定时器。 为了实现定时器的触发机制,我们可以采用轮询的方式。在程序的主循环中,我们使用一个循环遍历定时器队列,检查每个定时器的时间戳是否到达当前时间。如果到达,则调用该定时器对应的函数指针执行具体的任务。如果没有到达,则继续下一个定时器的检查。 在设计中,我们还需要考虑如何处理较短时间间隔的定时任务。可以使用一个最小时间间隔的变量来设置定时器的触发频率,以避免频繁触发定时器,降低程序效率。 此外,为了提高程序的效率,我们可以使用优先队列或二叉堆来组织定时器队列,以保证定时器触发顺序的准确性。 最后,我们还可以实现一些附加功能,如定时器暂停、恢复等,以满足不同的需求。 通过这样一个设计,我们可以灵活地管理和控制定时任务,提高程序的时间管理能力和效率。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值