Design and implementation of muduo library learning 03-TimerQueue timer

Dongyang's study notes

1. Add timer function to EventLoop.

  • Traditional Reactor realizes timing by controlling the waiting time of select() and poll()
  • Now in Linux timerfd, we can deal with timing in the same way as IO events, and the consistency of the code is better.

1.1 timerfd

There are many articles on timerfd on the Internet, so I won't summarize them. See:
https://www.cnblogs.com/mickole/p/3261879.html

二、TimerQueue class

The timer function of muduo is implemented by three classes, TimerId, Timer, and TimerQueue . Users can only see the first class, and the other two are implementation details. The implementation of TimerId and Timer is very simple, just talk about it hereTimerQueue

  • The interface of TimerQueue is very simple, with only two functions addTimer() and cancel().
class TimerQueue : boost::noncopyable
{
     
     
public:
 TimerQueue(EventLoop* loop);
 ~TimerQueue();

 ///
 /// Schedules the callback to be run at given time,
 /// repeats if @c interval > 0.0.
 ///
 /// Must be thread safe. Usually be called from other threads.
 TimerId addTimer(const TimerCallback& cb,
                  Timestamp when,
                  double interval);

 // void cancel(TimerId timerId);

2.1 Selection of TimerQueue's data structure

TimerQueue requires:

  • Efficiently organize Timers that have not yet expired
  • Can quickly find the expired Timer according to the current time
  • Efficiently add and delete Timer

2.1.1 The data structure is a linear table sorted by expiration time

The common operation of this structure is linear search, and the complexity is O(N)

2.1.2 Binary heap organization priority queue

The complexity of this approach is reduced to O(logN), but functions such as make_heap() of the C++ standard library cannot efficiently delete an element in the heap, and we need to implement it ourselves:

  • Remember to make Timer themselves in heapposition

2.1.3 Binary search tree

Sort the Timers in order of expiration time. The complexity of the operation is still there O(logN), but the memory locality is a bit worse than the heap, and the actual speed may be slightly slower.

TimerQueue actually uses the second type:

  • This structure uses the ready-made container library, is simple to implement , easy to verify its correctness , and has good performance
  • Use set instead of map, because there is only key without value. TimerQueue uses a Channel to observe the readable events on timerfd_. Note that the member functions of TimerQueue can only be called in the IO thread it belongs to 因此不必加锁.
  • Here the use of bare hands, can be used in the 11 ++ C unique_ptr, avoid manual resource management
private:

 // FIXME: use unique_ptr<Timer> instead of raw pointers.
 typedef std::pair<Timestamp, Timer*> Entry;
 typedef std::set<Entry> TimerList;

 // called when timerfd alarms
 void handleRead();
 // move out all expired timers
 std::vector<Entry> getExpired(Timestamp now);                  // 此处进行了ROV优化
 void reset(const std::vector<Entry>& expired, Timestamp now);

 bool insert(Timer* timer);

 EventLoop* loop_;
 const int timerfd_;
 Channel timerfdChannel_;
 // Timer list sorted by expiration
 TimerList timers_;
}; 

2.2 TimeQueue::getExpired()

This function from timers_removing expired Timer and returns the removed Timers by vector.

  • The compiler will implement RVO(compiler return value optimization), so you don't have to worry too much about the performance issues of writing in this way. If necessary, you can reuse vector like EventLoop::activateChannels_. Pay attention to 哨兵值(sentry)the selection
  • sentryLet set::lower_bound() return an 第一个iterator of an unexpired Timer, so the assert in &6 is <instead of <=
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
     
     
 std::vector<Entry> expired;
 Entry sentry = std::make_pair(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
 TimerList::iterator it = timers_.lower_bound(sentry);
 assert(it == timers_.end() || now < it->first);
 std::copy(timers_.begin(), it, back_inserter(expired));
 timers_.erase(timers_.begin(), it);

 return expired;
}

The following figure is the timing diagram of TimerQueue callback code onTimer():
Insert picture description here

3. Changes to EventLoop

EventLoop has added several new timer interfaces that are convenient for users to use. These functions all call TimerQueue::addTimer() instead.

  • Note that these EventLoop member functions should be allowed to be used across threads. For example, I want to execute a timeout callback in a certain IO thread.
  • Muduo's solution to the thread safety issues caused by this is not to lock , but to operate on TimerQueue 转移到 IO 线程(Base IO Thread)来进行(EventLoop has only one IO thread, so there will be no thread safety issues). This will use the EventLoop::runInLoop() function.
TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
     
     
 return timerQueue_->addTimer(cb, time, 0.0);
}

TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
     
     
 Timestamp time(addTime(Timestamp::now(), delay));
 return runAt(time, cb);
}

TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
     
     
 Timestamp time(addTime(Timestamp::now(), interval));
 return timerQueue_->addTimer(cb, time, interval);
}

Guess you like

Origin blog.csdn.net/qq_22473333/article/details/113657152