Use priority_queue implements a small top heap timer

Keywords: C++ Back-end

Timer module design

1 Introduction to timer module

Timer module is a common component in the server. This paper takes you to implement a timer module with basic functions

To design a Timer module, it generally includes two parts: one is the Timer object (Timer), and the other is the manager (TimerManager) who manages the Timer object (also known as Timer container);

2 timer object design

A Timer object is the packaging of a Timer;

id, expiration time and scheduled event callback are the basic members of the core;

Interval, timing times, initial_id_, mutex_ It is added to enrich timer functions, but this is also required in general timer modules;

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    //Judge whether the timer expires
    {
        return time(nullptr) >= expired_time_;
    }
  
    //An interface for a set of member variables
    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 use
    void Run();    //Execute callback
    static int64_t generateId();    //For constructor calls
    
private:
    int64_t id_;    //Timer id, so that the application and TimerManager can easily use the timer
    time_t expired_time_;    //Expiration time
    int32_t repeated_times_;    //Timing times
    int64_t interval_;    //Timer Intervals 

    TimeEventCallback callback_;    //Execution callback when a scheduled event occurs

    static int64_t initial_id_;    //For generating timer id, set it as a static data member to want each timer to have a unique id
    static std::mutex mutex_;    //Used to protect initial_id_
};

Timer implementation:

int64_t Timer::initial_id_ = 0;
std::mutex Timer::mutex_{};
void Timer::Run()
{
    callback_();
    if(repeated_times_ > 0)//When repeated_times_ == 0, the TimerManager::CheckAndHandle function will delete the timer
    {
        repeated_times_--;
    }
    expired_time_ += interval_;
}
int64_t Timer::generateId()
{
    std::lock_guard lk(mutex_);//Because there may be multiple threads through the operation initial_id_, So you need to use a mutex
    initial_id_++;
    return initial_id_;
}

3 timer manager design

OK, now we can create n timers in the main program, but how to manage it? How to add a new timer, manually delete an old timer, and check whether these timers have expired? If you put this part of the code into the main program, it is too complicated, so you abstract it and write a timermanager class;

A TimeManager object manages all timer objects in the thread, so there must be a data structure to organize these timer objects; (here I stand directly on the shoulders of giants and use priority_queue This article introduces the adapter , logical structure is heap)

In addition, the three core operations of TimeManager are to add a new timer, manually delete an old timer, and check and process the timer;

auto cmp = [](Timer* t1,Timer* t2){return t1->ExpiredTime() > t2->ExpiredTime();};//Customize the operator so that priority_ The earlier the timers in the queue become obsolete, the higher the number of timers in the queue

class TimerManager
{
public:
    using TimeEventCallback = std::function<void()>;
    TimerManager() = default;
    ~TimerManager() = default;
  	//Add timer
    int64_t AddTimer(int32_t repeatedtimes,int64_t interval,TimeEventCallback& callback);
    int64_t AddTimer(Timer* timer);
  
    void RemoveTimer(int64_t id);//Manually delete timer
    void CheckAndHandle();//Check and handle
private:
    std::priority_queue<Timer*,std::vector<Timer*>,decltype(cmp)> timers_queue_{cmp};//Manage the data structure of all timers

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

If the cmp is not understood, it is recommended to read it Custom operator

There is also an STD:: unordered in the data member_ map<int64_ t,Timer*> timer_ mp_; Storing the timer object id and the pointer to the timer object; Why should I have him? Because I found priority_ The queue can only access the top elements of the heap, so I used a map to record it, which is convenient to manually delete the timer;

If there is a better way, please point out! 🙏!

TimerManager implementation:

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);//In this way, the element to be deleted will sink to the top of the heap
    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 object is invalid, delete
            {
                timer_mp_.erase(delete_timer->Id());
                delete delete_timer;
                timers_queue_.pop();
            }
        }
        else
            return ;//The following timers do not need to be detected, because its timeout must be later than that of the current timer
    }
}

4 test code

  1. Test whether the timer adding function of TimerManager is normal
  2. Test whether the timer removal function of TimerManager is normal
  3. Test whether the timer function of TimerManager is normal
#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);//The passed in parameters are timing times, timing interval and execution callback

    //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 Summary

It can be seen that the logic of the timer module itself is not complex, but the efficiency should be considered, and what data structure should be used to reduce the time complexity of the above three basic operations;

Common data structures:

  1. Linked list and queue:
  2. map:
  3. Time wheel:
  4. Time heap:

The priority queue (heap) used in this paper: its search and deletion complexity is O(1); The time consumption is mainly in the automatic adjustment of the heap

6 errors in coding

std::priority_queue<Timer,std::vector<Timer>,decltype(cmp)> timers_queue_{cmp};//correct
std::priority_queue<Timer,std::vector<Timer>,decltype(cmp)> timers_queue_(cmp);//Error, error when he is a class member
  1. How does the small root heap remove the specified element? How to find the specified element?

    Add an unordered_ A map storing a timer object id and a pointer to the timer object;

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

The error code sets the access permission of the function to private

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

Static data members in a class need to be declared inside the class and initialized outside the class, but I did not mutex the static data member_ initialization

Todo: time complexity and performance comparison of each data structure;

reference material:

  1. Zhang Yuanlong - essence of C + + server development

Posted by batfink on Sun, 19 Sep 2021 13:36:22 -0700