Source code analysis of muduo Network Library: TimerQueue timing mechanism

Keywords: less Linux

Preface

The first three chapters make the log part clear. From this chapter, start with TimeQueue and introduce Runinloop. In the next or next chapter, describe the event distribution mechanism of the entire muduo

TimerQueue seems to be a troublesome class, but it's very simple. We can regard it as a time wheel. In fact, it can also act as a time wheel itself, but its efficiency is not as high as that of the time wheel. The time wheel can be close to O(1), while the TimeQueue in muduo uses set, and the insertion and removal (dichotomy) are O(logn), In addition, the biggest and most important difference is that the time wheel is signal driven, while the TimeQueue is event driven. A simple analysis is here. We can see more details from the source code

(class diagram)

Let's first look at the three most important members,

  TimerId addTimer(TimerCallback cb,
                   Timestamp when,
                   double interval);

  void cancel(TimerId timerId); 
  
  void handleRead(); 

Let's first think about what we need for the following general timing mechanism. Obviously, we need an insertion and deletion, which is exactly the addTimer and cancel we see above, and then there is a handleRead. Because TimerQueue is event driven, when we receive readable events, we naturally need to execute the callback from add, so the function of handleRead is clear

Let's take a look at the constructor first

TimerQueue::TimerQueue(EventLoop* loop) 
  : loop_(loop),
    timerfd_(createTimerfd()), //Create a Timerfd
    timerfdChannel_(loop, timerfd_),//Create a channl object to join the Eventloop of IO thread
    timers_(),
    callingExpiredTimers_(false)
{
  timerfdChannel_.setReadCallback(
      std::bind(&TimerQueue::handleRead, this));//Register callback events
  // we are always reading the timerfd, we disarm it with timerfd_settime.
  timerfdChannel_.enableReading(); //Register readable events in IO multiplexing
}

In fact, there are many points to be said:

  1. There is only one constructor, that is, Eventloop type. The reason is related to the entire event distribution mechanism of muduo, which we will explain in the next article
  2. timerfd, which is interesting, is a new timer interface in Linux 2.6.25. The entity is a file descriptor, which means it is event based
  3. Calling expired timers? Let's focus on this mechanism
  4. channel object, that is = = timerfdChannel = = register callback, involves the event distribution mechanism of muduo, which will be explained in the next section

We've dug many holes. We'll fill them one by one. Let's start!

TimerId TimerQueue::addTimer(TimerCallback cb, //User defined callback
                             Timestamp when, //When triggered
                             double interval) //Repeat trigger interval less than 0 no repeat trigger
{
  Timer* timer = new Timer(std::move(cb), when, interval);
  loop_->runInLoop(
      std::bind(&TimerQueue::addTimerInLoop, this, timer));
  return TimerId(timer, timer->sequence()); //Uniquely identify a Timer
}

We can see that addTimer actually registers a callback. We don't care about runInLoop for the moment. I feel like I can write a special article , we will discuss later. This article focuses on the logical part of TimerQueue. We enter TimerQueue::addTimerInLoop,

void TimerQueue::addTimerInLoop(Timer* timer)
{
  loop_->assertInLoopThread();
  bool earliestChanged = insert(timer); //Insert Timers and activetimers (cancel)

  if (earliestChanged)
  {
    //If the minimum time limit in timers is updated, update the timer once
    resetTimerfd(timerfd_, timer->expiration());
  }
}

...............

bool TimerQueue::insert(Timer* timer)
{
  loop_->assertInLoopThread();
  assert(timers_.size() == activeTimers_.size());
  bool earliestChanged = false;
  Timestamp when = timer->expiration();
  /*Get the item with the shortest timing in the queue, i.e. the first one is that the data structure is set and the red black tree is in order,
  The comparison order is pair, i.e. first, second*/
  TimerList::iterator it = timers_.begin(); 
  if (it == timers_.end() || when < it->first) //There is no Timer in timers or the one whose Timer is less than the minimum
  {
    earliestChanged = true; //true indicates that the minimum value is updated after inserting timers as the first element
  }
  {
    std::pair<TimerList::iterator, bool> result
      = timers_.insert(Entry(when, timer)); //Even if the time stamp is the same, the address behind it must be different
    assert(result.second); (void)result; //Assertion is always true.
  }
  {
    std::pair<ActiveTimerSet::iterator, bool> result
      = activeTimers_.insert(ActiveTimer(timer, timer->sequence()));
    assert(result.second); (void)result; // What does TODO activeTimers do?
  }

  assert(timers_.size() == activeTimers_.size());
  return earliestChanged;
}

...............

void resetTimerfd(int timerfd, Timestamp expiration)
{
  // wake up loop by timerfd_settime()
  struct itimerspec newValue;
  struct itimerspec oldValue;
  memZero(&newValue, sizeof newValue);
  memZero(&oldValue, sizeof oldValue);
  newValue.it_value = howMuchTimeFromNow(expiration); //The difference between the given time and the present time is greater than 100 microseconds
  //The time from expiration to now is greater than or equal to 100 microseconds, that is to say, there must be an executable callback and a reset timer when timer FD is triggered
  int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);//Reset timer, the fourth parameter can be null
  if (ret) //Success return zero failure return - 1
  {
    LOG_SYSERR << "timerfd_settime()";
  }
}

We have noticed that a TimerId will be returned after insertion. We do not analyze this, but we just want to identify the unique object

Let's continue to see cancel. What we see is a callback

void TimerQueue::cancel(TimerId timerId)
{
  loop_->runInLoop(
      std::bind(&TimerQueue::cancelInLoop, this, timerId));
}

void TimerQueue::cancelInLoop(TimerId timerId)
{
  loop_->assertInLoopThread();
  assert(timers_.size() == activeTimers_.size());
  ActiveTimer timer(timerId.timer_, timerId.sequence_);
  ActiveTimerSet::iterator it = activeTimers_.find(timer);
  if (it != activeTimers_.end()) //Element to delete found in added collection
  {
    size_t n = timers_.erase(Entry(it->first->expiration(), it->first));
    assert(n == 1); (void)n;
    delete it->first; // FIXME: no delete please
    activeTimers_.erase(it); //153156 delete directly in timers and activeTimer 
  }
  else if (callingExpiredTimers_)
  {
    cancelingTimers_.insert(timer); //Join delete queue delete when triggered
  }
  assert(timers_.size() == activeTimers_.size());
}

Here's a key point to say: most people's question here is "calling expired timers". Why is it true to add events to the deleted set? We can see that in the first if judgment, events have actually been deleted from the existing set. We can't help but ask, "do we need canceling timers"? The answer is yes. See the following code, the third important function handleRead

void TimerQueue::handleRead()
{
  loop_->assertInLoopThread();
  Timestamp now(Timestamp::now());
  readTimerfd(timerfd_, now); //Read timeout because muduo default LT prevents multiple triggers

  std::vector<Entry> expired = getExpired(now); //Get the current timeout Timer RVO without worrying about efficiency

  callingExpiredTimers_ = true;
  cancelingTimers_.clear();
  // safe to callback outside critical section
  for (const Entry& it : expired)
  {
    it.second->run(); //Execute callback in Timer,
  }
  callingExpiredTimers_ = false;
  reset(expired, now);
}

Because when this function is triggered, we can make sure that Timerfd is readable. We can see that ReadTimerfd is read first, because muduo defaults to LT, and then gets the readable set through getExpired. Next, we will focus on callingExpiredTimers. Why use true and false to package here? The reason is that callbacks in run may execute cancelInLoop, At that time, there is no need to delete in timers, so information will be lost, so maintain a cancelingTimers. We will go back to cancelInLoop, and only when the callingExpiredTimers? Is true can we join the deletion collection, that is, cancelingTimers? When will we use it? After the handleRead execution, we will call back to execute reset

//Rejoin requests that can be repeated and do not want to be deleted 
void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)
{
  Timestamp nextExpire;

  for (const Entry& it : expired)
  {
    ActiveTimer timer(it.second, it.second->sequence());
    if (it.second->repeat()
        && cancelingTimers_.find(timer) == cancelingTimers_.end()) //Duplicate collection to delete not found
    {
      it.second->restart(now); //Reset timeout
      insert(it.second); //Reinsert it. erase is missing
    }
    else
    {
      // FIXME move to a free list
      delete it.second; // FIXME: no delete please / / it's not good to delete the naked pointer no matter which one is better than unique ﹐ PTR
    }
  }

  if (!timers_.empty())
  {
    nextExpire = timers_.begin()->second->expiration(); //Minimum expected time
  }

  if (nextExpire.valid()) //If the minimum number of time items is valid
  {
    resetTimerfd(timerfd_, nextExpire); //Reset Timer 
  }
}

cancelingTimers are used here_

Finally, the core of the working mechanism of TimerQueue, getExpired, is how to retrieve the time after triggering the readable time, which is also one of the biggest differences with the time round

std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
  assert(timers_.size() == activeTimers_.size());
  std::vector<Entry> expired;
  //Uintptr? Max is the maximum value of objects of type uintptr? T, which is to put the same value at all times when the time is the same 
  Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));
  TimerList::iterator end = timers_.lower_bound(sentry);
  assert(end == timers_.end() || now < end->first);
  std::copy(timers_.begin(), end, back_inserter(expired));
  timers_.erase(timers_.begin(), end);

  for (const Entry& it : expired)
  {
    ActiveTimer timer(it.second, it.second->sequence());
    size_t n = activeTimers_.erase(timer); //Number of deleted items in activeTimers
    assert(n == 1); (void)n;
  }

  assert(timers_.size() == activeTimers_.size());
  return expired;
}

After finishing the member function, it is worth mentioning that the selection of data structure is STD:: pair < Timestamp, timer > Entry, which can ensure that when the timestamps are the same (which is the reason why map is not used), two places can be occupied in the container, because the address must be different (if the object is different), so why not use hash map, With the hash function provided by the library, we can ensure that different objects have different hash values. The reason is that we need an orderly data structure. Of course, the priority queue of the library is not good, because there is no way to delete objects. As for the multiplicity map, there is no need. If you can use set, there is no need to use that*

summary

Two interesting points

  1. Timerqueue selects the time to trigger. When the timeout is set, the previous items are removed. This is different from the time wheel
  2. Is activeTimers necessary? I've been reading it for a long time, and I've checked the data, but I still haven't figured out the problem

The last one,
Important things are to be repeated for 3 times!

event driven

event driven

event driven

110 original articles published, 69 praised, 10000 visitors+
Private letter follow

Posted by NTGr on Wed, 19 Feb 2020 22:55:17 -0800