使用priority_queue实现一个小顶堆定时器

定时器模块设计

1 定时器模块介绍

定时器模块是服务器中的常用组件,本文带你实现一个具有基本功能的定时器模块

要想设计一个定时器模块,一般包含两部分,一个是定时器对象(Timer),另一个管理定时器对象的管理者(TimerManager)(也叫定时器容器);

2 定时器对象设计

一个Timer对象就是对一个定时器的包装

其中id、过期时间、定时事件回调是核心的基本成员;

间隔、计时次数、initial_id_、mutex_是为了丰富定时器功能加上的,不过这也是一般定时器模块里所需要的;

class Timer
{
public:
    using TimeEventCallback = std::function<void()>;
    Timer(int32_t repeatedtimes,int64_t interval,TimeEventCallback& callback)
        :repeated_times_(repeatedtimes)
        ,interval_(interval)
        ,callback_(std::move(callback))
        {
            expired_time_ = time(nullptr) + interval_;
            id_ = generateId();
        }
    ~Timer() = default;
    
    bool IsExpired() const    //判断该定时器是否到期
    {
        return time(nullptr) >= expired_time_;
    }
  
    //一组成员变量的接口
    time_t ExpiredTime()
    {
        return expired_time_;
    }
    int64_t Id()
    {
        return id_;
    }
    int32_t RepeatTimes() {return repeated_times_;}

    void SetExpiredTime(int64_t t) {expired_time_ = t;}    //TimerManager::RemoveTimer使用
    void Run();    //执行回调
    static int64_t generateId();    //供构造函数调用
    
private:
    int64_t id_;    //定时器id,让应用程序和TimerManager能方便的使用该定时器
    time_t expired_time_;    //过期时间
    int32_t repeated_times_;    //计时次数
    int64_t interval_;    //计时间隔

    TimeEventCallback callback_;    //定时事件发生时的执行回调

    static int64_t initial_id_;    //用于生成定时器id的,将它设置为静态数据成员,是想要每个定时器都有唯一的id
    static std::mutex mutex_;    //用于保护initial_id_
};

Timer实现:

int64_t Timer::initial_id_ = 0;
std::mutex Timer::mutex_{};
void Timer::Run()
{
    callback_();
    if(repeated_times_ > 0)//当repeated_times_ == 0,TimerManager::CheckAndHandle函数就会删除该定时器
    {
        repeated_times_--;
    }
    expired_time_ += interval_;
}
int64_t Timer::generateId()
{
    std::lock_guard lk(mutex_);//因为可能有多个线程通过操作initial_id_,所以需要使用互斥锁
    initial_id_++;
    return initial_id_;
}

3 定时器管理者设计

好了,现在我们可以在主程序中创建n个定时器了,但是怎么管理它呢?如何添加一个新的定时器、手动删除一个旧的定时器、检查这些定时器是否已经到期?如果把这部分代码放到主程序中,那么也太繁杂了,所以就抽象出来写一个TimerMananger类;

一个TimeManager对象管理该线程内的所有定时器对象,所以必有一个数据结构来组织这些定时器对象;(这里我直接站在巨人的肩膀上使用priority_queue这篇文章有介绍该适配器,逻辑结构就是堆)

此外,TimeManager的三种核心操作就是添加一个新的定时器、手动删除一个旧的定时器、检查并处理定时器

auto cmp = [](Timer* t1,Timer* t2){return t1->ExpiredTime() > t2->ExpiredTime();};//自定义算子,使得priority_queue中的越早过时的定时器排在前面

class TimerManager
{
public:
    using TimeEventCallback = std::function<void()>;
    TimerManager() = default;
    ~TimerManager() = default;
  	//添加定时器
    int64_t AddTimer(int32_t repeatedtimes,int64_t interval,TimeEventCallback& callback);
    int64_t AddTimer(Timer* timer);
  
    void RemoveTimer(int64_t id);//手动删除定时器
    void CheckAndHandle();//检查并处理
private:
    std::priority_queue<Timer*,std::vector<Timer*>,decltype(cmp)> timers_queue_{cmp};//管理所有定时器的数据结构

    std::unordered_map<int64_t,Timer*> timer_mp_;
};

如果cmp哪里没看懂,建议看自定义算子

数据成员中还多了一个std::unordered_map<int64_t,Timer*> timer_mp_;存放的是定时器对象id 与 指向该定时器对象的指针;为什么要有他呢,因为我发现priority_queue只能访问堆顶元素,所以我用了一个map来记录一下,方便手动删除定时器;

如果这里有更好的方法,请大佬指出!🙏!

TimerManager实现:

int64_t TimerManager::AddTimer(int32_t repeatedtimes,int64_t interval,TimeEventCallback& callback)
{
    Timer* t = new Timer(repeatedtimes,interval,callback);
    timers_queue_.push(t);
    timer_mp_[t->Id()] = t;
    return t->Id();
}
int64_t TimerManager::AddTimer(Timer* timer)
{
    int id = timer->Id();
    timer_mp_[id] = timer;
    timers_queue_.push(timer);
    return id;
}
void TimerManager::RemoveTimer(int64_t id)
{
    Timer* temp = timer_mp_[id];
    temp->SetExpiredTime(-1);//这样要被删除的元素就会上沉到堆顶
    timers_queue_.pop();
    timer_mp_.erase(id);

}
void TimerManager::CheckAndHandle()
{
    
    int len = timers_queue_.size();
    for(int i=0;i<len;i++)
    {
        Timer* delete_timer;
        delete_timer = timers_queue_.top();
        if(delete_timer->IsExpired())
        {
            delete_timer->Run();
            if(delete_timer->RepeatTimes() == 0)//定时器对象无效了,删除
            {
                timer_mp_.erase(delete_timer->Id());
                delete delete_timer;
                timers_queue_.pop();
            }
        }
        else
            return ;//后面的定时器都不需要检测了,因为它的超时时间肯定比当前定时器的超时时间晚
    }
}

4 测试代码

  1. 测试一下TimerManager的添加定时器功能是否正常
  2. 测试一下TimerManager的移除定时器功能是否正常
  3. 测试一下TimerManager的定时器功能是否正常
#include "timer_manager.h"
#include <iostream>
#include <functional>

#include <unistd.h>
using namespace std;
void TimeFunc1()
{
    std::cout<< "Timer1 Timer On!" <<std::endl;
}
void TimeFunc2()
{
    std::cout<< "Timer2 Time On!" <<std::endl;
}
int main()
{
    TimerManager time_manager;
    
    std::function<void()> f1 = TimeFunc1;
    time_manager.AddTimer(3,4,f1);

    std::function<void()> f2 = TimeFunc2;
    Timer timer2(4,1,f2);
    int timer_id2 = time_manager.AddTimer(&timer2);//传入参数分别为计时次数、计时间隔、执行回调

    //while(1)
    for(int i=0;i<8;i++)
    {
        time_manager.CheckAndHandle();
        std::cout << "doing other things! cost 2s!" << std::endl;
        sleep(2);
        if(i == 1)
        {
            time_manager.RemoveTimer(timer_id2);
            std::cout<< "Timer2 has been removed! " <<std::endl;
        }
    }
    return 0;
}
/*
doing other things! cost 2s!
Timer2 Time On!
Timer2 Time On!
doing other things! cost 2s!
Timer2 has been removed! 
Timer1 Timer On!
doing other things! cost 2s!
doing other things! cost 2s!
Timer1 Timer On!
doing other things! cost 2s!
doing other things! cost 2s!
Timer1 Timer On!
doing other things! cost 2s!
doing other things! cost 2s!
*/

5 总结

可以看到定时器模块本身逻辑并不复杂,而是要考虑效率的问题,采用何种数据结构,来使得以上三种基本操作的时间复杂度较小;

常用的数据结构:

  1. 链表、队列:
  2. map:
  3. 时间轮:
  4. 时间堆:

本文中采用的优先队列(堆):其查找、删除的复杂度均为O(1);消耗时间主要在堆的自动调整上

6 编码过程中的错误

std::priority_queue<Timer,std::vector<Timer>,decltype(cmp)> timers_queue_{cmp};//正确
std::priority_queue<Timer,std::vector<Timer>,decltype(cmp)> timers_queue_(cmp);//错误,当他为类成员时出错
  1. 小根堆如何移除指定元素?如何找到指定元素?

    添加一个unordered_map,存储定时器对象id 与 指向该定时器对象的指针;

Undefined symbols for architecture arm64:
“Timer::mutex_”, referenced from:
Timer::generateId() in timer_manager.cc.o
ld: symbol(s) not found for architecture arm64

错误代码将该函数的访问权限设置private

Undefined symbols for architecture arm64:
“Timer::mutex_”, referenced from:
Timer::generateId() in timer.cc.o
ld: symbol(s) not found for architecture arm64

类中的静态数据成员需要在类内声明,类外初始化,而我没有对静态数据成员mutex_初始化


Todo:各数据结构的时间复杂度和性能对比;

参考资料:

  1. 张远龙 - C++服务器开发精髓
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值