分级时间轮优化普通时间轮定时器
《实现较低的计时器粒度以重传TCP(RTO):时间轮算法如何减少开销》
Table of Contents
Ratas-分级计时器轮
(分级)计时器轮
单文件实现,无依赖性
限制单个时间步中触发的事件数
优化高占用率,而不是低
基于范围的调度
基准程序
没有做到的功能
脚注
timer-wheel.h
Ratas-分级计时器轮
上周,我需要一个计时器轮来进行业余爱好项目。在过去的三十年中,这种数据结构已经不断地被重新实现,但是由于种种原因,我对任何免费的数据结构都不感到兴奋。显然,这意味着需要更多实现,因此需要 Ratas-分层计时器轮。不幸的是,我的假期用完了,才回到原来的项目,但这就是牛剃须的本质。
在这篇文章中,我将首先简要地解释什么是计时器轮-如果您有时间,您可能想阅读其中的一本参考书-然后进一步详细说明我为何撰写新的计时器。
(分级)计时器轮
计时器轮是实现计时器队列的一种方法,依次用于安排事件在将来某个时间发生的事件。如果您有数十个或数百个计时器,它们的存储方式无关紧要。未排序的列表就可以了。要处理数百万个计时器,您需要一些更复杂的东西。
计时器轮实际上是链接的事件列表的环形缓冲区,也是指向环形缓冲区的指针。每个插槽对应一个特定的计时器刻度,并包含一个链表的头部。链接列表包含应在该刻度上发生的事件。像这样
(指针为红色,轮槽为灰色,事件为橙色,数字显示与该时间/事件关联的时间。)
对于每一刻,指针都会向前移动一个插槽。现在,已通过的广告位会将完整轮转指向未来。第一个插槽不再是刻度0,而是刻度8:
插槽中的任何事件都将在指针传递时执行。因此,在下一个滴答事件中,将执行事件a和b,并将其从环中删除。
对于插入或删除计时器的重要操作,计时器轮具有O(1)时间复杂度和便宜的常数因子。各种排序序列(列表,树,堆)的缩放比例将变差,并且常数因子趋向于更大。但是基本的计时器轮只能在有限的时间范围内工作。问题是当计时器范围大于环的大小时如何扩展它们的工作。
一种解决方案是分层计时器轮,该计时器轮将以不同分辨率运行的多个简单计时器轮彼此叠加。每个字段都有自己的插槽和指针。
如果某个事件在将来的时间安排得足够远,以致于它无法容纳最里面的(核心)轮子,那么它将安排在一个外面的轮子上。因此,如下所示,为第9和第13滴答声安排的计时器已存储在第二个滚轮的第一个插槽中:
通常,计时器刻度线只会将指针移到核心轮上,其工作原理与简单的计时器轮一样。在此示例中,直到并包括第7个刻度,这都是正确的。
但是,当核心轮的指针回绕时,第二轮的指针将前进一个插槽。该插槽中包含的事件将被执行或提升为核心环中的正确插槽。(事件e和f在这里发生)。
这显然可以推广到两层以上。任何单个轮子的完整旋转都会使指针在下一层轮子上前进一个。
1987年的论文Hashed and Hierarchical Timing Wheels:高效实现计时器功能的数据结构(由Varghese&Lauck提出)是层次定时轮[ 0 ]的概念。和往常一样,如果您没有时间阅读全文,则Adrian Colyer的摘要非常有用 。
单文件实现,无依赖性
我不能使用现有实现的主要原因是,最好的实现已深深地嵌入到较大的系统中,因此需要大量工作才能提取到独立的库中。Ratas是单文件实现,没有C ++ 11之外的任何外部依赖项。因此,加入任何C ++项目应该非常容易。
在相关但同样重要的一点上,Ratas没有任何内部时间源,它的时间概念由图书馆的用户从外部驱动。实际上,这是一个比可能首先出现的更重要的属性。许多事件循环库都具有某种类型的计时器队列,但是事件循环本质上希望控制执行。我需要一个组件来代替构建自定义事件循环[ 1 ]。
我看过的某些库具有有趣的实现策略。(哇,DPDK使用跳过列表作为计时器队列?)。运行一些比较所有这些实现的确定性基准本来很好。但是总的来说,即使要从其中提取最低限度的可行的独立实现的粗暴黑客工作也是太多的工作。
限制单个时间步中触发的事件数
过去我想要执行的一项操作是限制对计时器advance
方法的单次调用中触发的计时器数量。如果超出了该范围,计时器轮应尽早松开,让应用程序执行一些工作,然后从停止的地方继续。这样,不幸的是,大量的定时器无法使主处理循环饿死太长时间。[ 2 ]
当然,主应用程序循环将需要特殊的逻辑逻辑,并且在处理积压之前,对计时器处理的调用要比正常情况下更为频繁。
对于完全排序的事件队列,这是微不足道的。对于计时器轮,这意味着使计时器处理整体上成为可重入的(包括在刻度线之间仍处于滴答状态时修改的轮的内容)。幸运的是,有一种方法可以做到这一点,只需要很少的额外滚轮级状态,而无需对事件调度或取消逻辑进行任何更改。因此,快速路径上的开销几乎无法衡量。
优化高占用率,而不是低
计时器轮的感知问题之一是,虽然事件插入和删除是O(1)操作,但找出直到下一个事件触发为止的剩余时间是O(m + n),其中m是计时器轮槽的总数,并且n是事件数。具体来说,您需要穿过wheel,直到找到带有一些事件的插槽。然后,根据插槽的分辨率,您可能需要遍历该插槽的整个事件链。除了算法复杂性外,这两个操作都有效地追踪了指针,因此将有很多缓存污染。
有许多技巧可以使此操作更便宜。例如,每个轮子可以具有与轮子中的插槽阵列平行的位图。如果位图在给定位置具有1,则该插槽为非空。这允许实现方案通过完全跳过空插槽来缩短搜索。
我不认为此类优化是一个很好的权衡。他们加快了一些操作:查找下一个事件的时间,一次将时间增加很多滴答,但前提是计时器轮利用率低。作为交换,您需要为每次插入和删除操作支付少量的额外费用。[ 4 ]
这就是事情。如果转轮利用率低,则意味着程序整体上利用率低。正是这种情况,我不在乎这样的组件的性能。系统负载后,性能才开始变得重要。当整个系统承受重负荷时,计时器轮也是如此。到那时,这些查找操作几乎会立即短路,因此优化没有任何作用。另一方面,这恰恰是增加插入和删除操作的额外开销最明显的时候。
但是,我们可以在界面上做一件事来帮助解决原始问题。每次我使用过任何类似的ticks_until_next_event()
功能时,它都会带有以下模式:
Tick sleep_usec = std::min(timers.ticks_until_next_event(), 1000);
我感兴趣的结果有一个上限。即使下一个毫秒内没有计时器事件要处理,我也知道会发生一些非计时器驱动的事情。因此,让我们告诉计时器轮的上限是什么:
Tick sleep_usec = timers.ticks_until_next_event(1000);
这样,计时器轮便会缩短搜索范围,并在很明显计时器轮不包含计划在该阈值之前发生的事件时立即返回。
降低成本的另一种方法ticks_until_next_event
是允许其返回下限而不是准确的结果。如果进程到达一个具有多个滴答滴答声的插槽,则不要遍历事件链,而是返回该插槽滴答声范围的下限。我没有再这样做,因为感觉就像是针对无用的情况进行了优化,并且因为API将需要分别的精确和近似操作。(仅提供近似操作似乎是错误的)。
基于范围的调度
许多计时器事件一遍又一遍地安排,但从未执行[ 3 ]。通常,执行它们的确切时间无关紧要,但是由于计时器是按分钟量调整的,因此会有很多昂贵的变动。为了减少流失,Ratas包括第二个调度界面,该界面采用一定范围的可接受时间而不是单个准确时间。例如:
timers.schedule_in_range(event, 100000, 101000);
然后由实施来决定最佳调度。现在,的实现会schedule_in_range
立即在一个滴答滴答声中决定,以安排事件的时间。此后,该范围内的所有信息都将丢失,而不是长期保持。该决定当前执行如下:
- 如果已经在正确的时间间隔中安排了计时器,则什么也不做。
- 最好将计时器安排在确切的槽口边界上,这样就可以就地执行而不是升级到内轮。
- 最好将计时器安排在尽可能晚的范围内。这对于最大化第一点的效率很重要。
为什么要立即调整时间并丢弃范围信息?主要是因为目前尚不清楚,稍后有适当的时间使用这些信息。我们将再次关注该事件的主要时间是何时执行该事件,或何时对其进行重新安排。在第一种情况下,范围什么也没有给我们。在第二种情况下,我们已经获得了新的改进范围。
使用这个额外的界面并没有像我在基准测试中所希望的那么大,只有大约10%。原因是我的四种计时器类型中只有一种与基于范围的调度非常匹配。但是我不知道它是否适合更大比例的现实用例,所以这很公平。
近似事件调度无论如何都不是一个新概念[5]。而且即使计时器库不支持近似调度,您也可以在应用程序级别上模拟它的某些部分。但是即使如此,您似乎仍希望将其正确集成到库中。原生功能总是比包装器更舒适。
最后,还有另一种替代策略可以减轻计时器逐渐变慢的麻烦。如果计时器已经处于活动状态,并且将其重新安排到将来使用,请更新其安排的时间,但是将事件结构放在环上的完全相同的位置。当事件发生在转轮上时,计时器轮会检查执行时间,注意到该时间仍在将来,然后将事件重新安排到适当的位置,而不是执行回调。
该理论听起来很合理,但是在我的测试中,这主要是导致了较小的减速。它当然取决于工作负载,但是它绝对脆弱,无法用作默认值,并且太晦涩难懂,因此不值得选择。我也讨厌失去不变的观点,即插槽仅包含具有正确刻度的事件。
基准程序
我写了一个小基准,它创建了可配置数量的事件,并混合了不同的行为:
- 持续时间短的计时器会不断进行调度,并且几乎总是执行。
- 时间长的计时器被安排一次,最终被执行。
- 持续时间长的计时器会不断地重新安排在以后发生,因此永远不会执行。
- 具有中等持续时间的计时器很少进行计划,并且每次重新计划时都会执行一次。
- 具有中等持续时间的计时器很少被安排,或者被重新安排为更早发生或被取消。
这些计时器的集合被分组为单元,每个单元总共包含10个独立的计时器事件。工作单元以略微不同的周期长度运行,以使访问模式随时间变化。最后还有很长的完全空闲时间。
通过使用各种不同数量的“工作单位”运行基准测试,得出以下结果。每个单元总共包含10个计时器(但并非所有计时器都同时处于活动状态),并且在测试期间,将安排约70k个事件并最终执行约23k个事件。在虚拟计时器滴答声中测得的测试运行时间始终是恒定的,因此增加工作单位的数量将增加平均车轮占用率,而不是使更多工作连续进行。
这是一个对数-对数图,即两个轴都是对数的。尽管理论上所有重要的操作都是固定的时间,但是性能却非线性下降,并确实影响了256k个工作单元,工作量加倍将运行时间增加了5倍。这几乎肯定是缓存问题。我在其上进行测试的i7-2640M具有4MB缓存,因此对于128k工作单元而言,即使将事件保留在L3的核心轮上也已经很困难。因此,可能会出现次佳的缩放比例。
就绝对性能数字而言,32k工作单元工作负载将执行约1.2亿次调度操作,加上每秒约4000万个事件执行。最重要的是,应用程序在事件处理程序中所做的次要工作。对于使用5年的笔记本电脑CPU的一个内核来说,这似乎是不错的选择。
没有比较基准,对不起。正如我之前提到的,很难找到任何基准。我的基准程序似乎在我发现的唯一功能齐全的独立计时器队列中触发了某种性能错误,这使其执行速度比实际应有的速度慢了两个数量级。原始操作比这要快得多。我敢肯定问题会得到解决,但是现在并不能进行非常有用的比较。
没有做到的功能
故意不支持重复计时器。我认为这种事情应该由计时器明确地重新安排自己的时间来完成。
由于虚拟execute()
方法,计时器事件具有vtable 。最初,所有内容都是在编译时通过回调类型进行参数化的,因此单个轮子中的所有事件都可以使用相同的execute()
。但这确实不适用于MemberTimerEvent
,其中回调是静态指定的成员函数和动态指定的对象的组合。我不愿意放弃该功能,因此vtable的危害较小。
但是允许TimerWheel
使用特定参数进行参数化可能很好TimerEvent
。如果您需要异类轮,请使用current实例化模板 TimerEventInterface
。如果您可以使用同质轮子,请使用其他非virtual实例化实例化 execute()
。我之所以没有这样做,是因为它超出了我个人模板的容忍度,并且由于同种轮子无论如何都感觉不到是非常引人注目的用例。
脚注
- [ 0 ]我使用术语“计时器轮”而不是原始的“定时轮”。在看到本文标题之前,我一直会看到它们被称为前者。
- [ 1 ]我之前写过关于为何数据包处理应用程序可能需要与典型服务器应用程序不同的事件循环的知识,而在另一时间,我又谈到了为什么确定性的时间控制对于可测试性很重要。
- [ 2 ]有趣的是,这与操作系统内核想要做的相反。他们希望将尽可能多的计时器分批处理。此处的区别在于,我正在考虑一个单线程非锁定应用程序,而现代操作系统都与并发有关。
- [ 3 ]考虑一个计时器,该计时器在一段时间闲置后释放资源。在对资源执行每个单独的操作之后,可以重新安排这些时间。
- [ 4 ]也许成本不那么低;至少我发现很难在不为每个事件结构或每个插槽添加额外的反向指针的情况下维护位图,这两个都是不好的消息。您可以通过要求通过计时器轮取消计时器来在没有该反向指针的情况下执行此操作,但是这样就无法使计时器在销毁时自动取消。那将是完全不能接受的。
- [ 5 ]例如,Linux已经应用了基于百分比的计时器松弛,尽管其目的是尝试将尽可能多的计时器执行分批处理。有关 建议更换Linux计时器轮的LWN文章很好阅读。
timer-wheel.h
// -*- mode: c++; c-basic-offset: 4 indent-tabs-mode: nil -*- */
//
// Copyright 2016 Juho Snellman, released under a MIT license (see
// LICENSE).
//
// SPDX-License-Identifier: MIT
//
// A timer queue which allows events to be scheduled for execution
// at some later point. Reasons you might want to use this implementation
// instead of some other are:
//
// - A single-file C++11 implementation with no external dependencies.
// - Optimized for high occupancy rates, on the assumption that the
// utilization of the timer queue is proportional to the utilization
// of the system as a whole. When a tradeoff needs to be made
// between efficiency of one operation at a low occupancy rate and
// another operation at a high rate, we choose the latter.
// - Tries to minimize the cost of event rescheduling or cancelation,
// on the assumption that a large percentage of events will never
// be triggered. The implementation avoids unnecessary work when an
// event is rescheduled, and provides a way for the user specify a
// range of acceptable execution times instead of just an exact one.
// - Facility for limiting the number of events to execute on a
// single invocation, to allow fine grained interleaving of timer
// processing and application logic.
// - An interface that at least the author finds convenient.
//
// The exact implementation strategy is a hierarchical timer
// wheel. A timer wheel is effectively a ring buffer of linked lists
// of events, and a pointer to the ring buffer. As the time advances,
// the pointer moves forward, and any events in the ring buffer slots
// that the pointer passed will get executed.
//
// A hierarchical timer wheel layers multiple timer wheels running at
// different resolutions on top of each other. When an event is
// scheduled so far in the future than it does not fit the innermost
// (core) wheel, it instead gets scheduled on one of the outer
// wheels. On each rotation of the inner wheel, one slot's worth of
// events are promoted from the second wheel to the core. On each
// rotation of the second wheel, one slot's worth of events is
// promoted from the third wheel to the second, and so on.
//
// The basic usage is to create a single TimerWheel object and
// multiple TimerEvent or MemberTimerEvent objects. The events are
// scheduled for execution using TimerWheel::schedule() or
// TimerWheel::schedule_in_range(), or unscheduled using the event's
// cancel() method.
//
// Example usage:
//
// typedef std::function<void()> Callback;
// TimerWheel timers;
// int count = 0;
// TimerEvent<Callback> timer([&count] () { ++count; });
//
// timers.schedule(&timer, 5);
// timers.advance(4);
// assert(count == 0);
// timers.advance(1);
// assert(count == 1);
//
// timers.schedule(&timer, 5);
// timer.cancel();
// timers.advance(4);
// assert(count == 1);
//
// To tie events to specific member functions of an object instead of
// a callback function, use MemberTimerEvent instead of TimerEvent.
// For example:
//
// class Test {
// public:
// Test() : inc_timer_(this) {
// }
// void start(TimerWheel* timers) {
// timers->schedule(&inc_timer_, 10);
// }
// void on_inc() {
// count_++;
// }
// int count() { return count_; }
// private:
// MemberTimerEvent<Test, &Test::on_inc> inc_timer_;
// int count_ = 0;
// };#ifndef RATAS_TIMER_WHEEL_H
#define RATAS_TIMER_WHEEL_H#include <cassert>
#include <cstdlib>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <memory>typedef uint64_t Tick;class TimerWheelSlot;
class TimerWheel;// An abstract class representing an event that can be scheduled to
// happen at some later time.
class TimerEventInterface {
public:TimerEventInterface() {}// TimerEvents are automatically canceled on destruction.virtual ~TimerEventInterface() {cancel();}// Unschedule this event. It's safe to cancel an event that is inactive.inline void cancel();// Return true iff the event is currently scheduled for execution.bool active() const {return slot_ != NULL;}// Return the absolute tick this event is scheduled to be executed on.Tick scheduled_at() const { return scheduled_at_; }private:TimerEventInterface(const TimerEventInterface& other) = delete;TimerEventInterface& operator=(const TimerEventInterface& other) = delete;friend TimerWheelSlot;friend TimerWheel;// Implement in subclasses. Executes the event callback.virtual void execute() = 0;void set_scheduled_at(Tick ts) { scheduled_at_ = ts; }// Move the event to another slot. (It's safe for either the current// or new slot to be NULL).inline void relink(TimerWheelSlot* slot);Tick scheduled_at_;// The slot this event is currently in (NULL if not currently scheduled).TimerWheelSlot* slot_ = NULL;// The events are linked together in the slot using an internal// doubly-linked list; this iterator does double duty as the// linked list node for this event.TimerEventInterface* next_ = NULL;TimerEventInterface* prev_ = NULL;
};// An event that takes the callback (of type CBType) to execute as
// a constructor parameter.
template<typename CBType>
class TimerEvent : public TimerEventInterface {
public:explicit TimerEvent<CBType>(const CBType& callback): callback_(callback) {}void execute() {callback_();}private:TimerEvent<CBType>(const TimerEvent<CBType>& other) = delete;TimerEvent<CBType>& operator=(const TimerEvent<CBType>& other) = delete;CBType callback_;
};// An event that's specialized with a (static) member function of class T,
// and a dynamic instance of T. Event execution causes an invocation of the
// member function on the instance.
template<typename T, void(T::*MFun)() >
class MemberTimerEvent : public TimerEventInterface {
public:MemberTimerEvent(T* obj) : obj_(obj) {}virtual void execute () {(obj_->*MFun)();}private:T* obj_;
};// Purely an implementation detail.
class TimerWheelSlot {
public:TimerWheelSlot() {}private:// Return the first event queued in this slot.const TimerEventInterface* events() const { return events_; }// Deque the first event from the slot, and return it.TimerEventInterface* pop_event() {auto event = events_;events_ = event->next_;if (events_) {events_->prev_ = NULL;}event->next_ = NULL;event->slot_ = NULL;return event;}TimerWheelSlot(const TimerWheelSlot& other) = delete;TimerWheelSlot& operator=(const TimerWheelSlot& other) = delete;friend TimerEventInterface;friend TimerWheel;// Doubly linked (inferior) list of events.TimerEventInterface* events_ = NULL;
};// A TimerWheel is the entity that TimerEvents can be scheduled on
// for execution (with schedule() or schedule_in_range()), and will
// eventually be executed once the time advances far enough with the
// advance() method.
class TimerWheel {
public:TimerWheel(Tick now = 0) {for (int i = 0; i < NUM_LEVELS; ++i) {now_[i] = now >> (WIDTH_BITS * i);}ticks_pending_ = 0;}// Advance the TimerWheel by the specified number of ticks, and execute// any events scheduled for execution at or before that time. The// number of events executed can be restricted using the max_execute// parameter. If that limit is reached, the function will return false,// and the excess events will be processed on a subsequent call.//// - It is safe to cancel or schedule events from within event callbacks.// - During the execution of the callback the observable event tick will// be the tick it was scheduled to run on; not the tick the clock will// be advanced to.// - Events will happen in order; all events scheduled for tick X will// be executed before any event scheduled for tick X+1.//// Delta should be non-0. The only exception is if the previous// call to advance() returned false.//// advance() should not be called from an event callback.inline bool advance(Tick delta,size_t max_execute=std::numeric_limits<size_t>::max(),int level = 0);// Schedule the event to be executed delta ticks from the current time.// The delta must be non-0.inline void schedule(TimerEventInterface* event, Tick delta);// Schedule the event to happen at some time between start and end// ticks from the current time. The actual time will be determined// by the TimerWheel to minimize rescheduling and promotion overhead.// Both start and end must be non-0, and the end must be greater than// the start.inline void schedule_in_range(TimerEventInterface* event,Tick start, Tick end);// Return the current tick value. Note that if the time increases// by multiple ticks during a single call to advance(), during the// execution of the event callback now() will return the tick that// the event was scheduled to run on.Tick now() const { return now_[0]; }// Return the number of ticks remaining until the next event will get// executed. If the max parameter is passed, that will be the maximum// tick value that gets returned. The max parameter's value will also// be returned if no events have been scheduled.//// Will return 0 if the wheel still has unprocessed events from the// previous call to advance().inline Tick ticks_to_next_event(Tick max = std::numeric_limits<Tick>::max(),int level = 0);private:TimerWheel(const TimerWheel& other) = delete;TimerWheel& operator=(const TimerWheel& other) = delete;// This handles the actual work of executing event callbacks and// recursing to the outer wheels.inline bool process_current_slot(Tick now, size_t max_execute, int level);static const int WIDTH_BITS = 8;static const int NUM_LEVELS = (64 + WIDTH_BITS - 1) / WIDTH_BITS;static const int MAX_LEVEL = NUM_LEVELS - 1;static const int NUM_SLOTS = 1 << WIDTH_BITS;// A bitmask for looking at just the bits in the timestamp relevant to// this wheel.static const int MASK = (NUM_SLOTS - 1);// The current timestamp for this wheel. This will be right-shifted// such that each slot is separated by exactly one tick even on// the outermost wheels.Tick now_[NUM_LEVELS];// We've done a partial tick advance. This is how many ticks remain// unprocessed.Tick ticks_pending_;TimerWheelSlot slots_[NUM_LEVELS][NUM_SLOTS];
};// Implementationvoid TimerEventInterface::relink(TimerWheelSlot* new_slot) {if (new_slot == slot_) {return;}// Unlink from old location.if (slot_) {auto prev = prev_;auto next = next_;if (next) {next->prev_ = prev;}if (prev) {prev->next_ = next;} else {// Must be at head of slot. Move the next item to the head.slot_->events_ = next;}}// Insert in new slot.{if (new_slot) {auto old = new_slot->events_;next_ = old;if (old) {old->prev_ = this;}new_slot->events_ = this;} else {next_ = NULL;}prev_ = NULL;}slot_ = new_slot;
}void TimerEventInterface::cancel() {// It's ok to cancel a event that's not scheduled.if (!slot_) {return;}relink(NULL);
}bool TimerWheel::advance(Tick delta, size_t max_events, int level) {if (ticks_pending_) {if (level == 0) {// Continue collecting a backlog of ticks to process if// we're called with non-zero deltas.ticks_pending_ += delta;}// We only partially processed the last tick. Process the// current slot, rather incrementing like advance() normally// does.Tick now = now_[level];if (!process_current_slot(now, max_events, level)) {// Outer layers are still not done, propagate that information// back up.return false;}if (level == 0) {// The core wheel has been fully processed. We can now close// down the partial tick and pretend that we've just been// called with a delta containing both the new and original// amounts.delta = (ticks_pending_ - 1);ticks_pending_ = 0;} else {return true;}} else {// Zero deltas are only ok when in the middle of a partially// processed tick.assert(delta > 0);}while (delta--) {Tick now = ++now_[level];if (!process_current_slot(now, max_events, level)) {ticks_pending_ = (delta + 1);return false;}}return true;
}bool TimerWheel::process_current_slot(Tick now, size_t max_events, int level) {size_t slot_index = now & MASK;auto slot = &slots_[level][slot_index];if (slot_index == 0 && level < MAX_LEVEL) {if (!advance(1, max_events, level + 1)) {return false;}}while (slot->events()) {auto event = slot->pop_event();if (level > 0) {assert((now_[0] & MASK) == 0);if (now_[0] >= event->scheduled_at()) {event->execute();if (!--max_events) {return false;}} else {// There's a case to be made that promotion should// also count as work done. And that would simplify// this code since the max_events manipulation could// move to the top of the loop. But it's an order of// magnitude more expensive to execute a typical// callback, and promotions will naturally clump while// events triggering won't.schedule(event,event->scheduled_at() - now_[0]);}} else {event->execute();if (!--max_events) {return false;}}}return true;
}void TimerWheel::schedule(TimerEventInterface* event, Tick delta) {assert(delta > 0);event->set_scheduled_at(now_[0] + delta);int level = 0;while (delta >= NUM_SLOTS) {delta = (delta + (now_[level] & MASK)) >> WIDTH_BITS;++level;}size_t slot_index = (now_[level] + delta) & MASK;auto slot = &slots_[level][slot_index];event->relink(slot);
}void TimerWheel::schedule_in_range(TimerEventInterface* event,Tick start, Tick end) {assert(end > start);if (event->active()) {auto current = event->scheduled_at() - now_[0];// Event is already scheduled to happen in this range. Instead// of always using the old slot, we could check compute the// new slot and switch iff it's aligned better than the old one.// But it seems hard to believe that could be worthwhile.if (current >= start && current <= end) {return;}}// Zero as many bits (in WIDTH_BITS chunks) as possible// from "end" while still keeping the output in the// right range.Tick mask = ~0;while ((start & mask) != (end & mask)) {mask = (mask << WIDTH_BITS);}Tick delta = end & (mask >> WIDTH_BITS);schedule(event, delta);
}Tick TimerWheel::ticks_to_next_event(Tick max, int level) {if (ticks_pending_) {return 0;}// The actual current time (not the bitshifted time)Tick now = now_[0];// Smallest tick (relative to now) we've found.Tick min = max;for (int i = 0; i < NUM_SLOTS; ++i) {// Note: Unlike the uses of "now", slot index calculations really// need to use now_.auto slot_index = (now_[level] + 1 + i) & MASK;// We've reached slot 0. In normal scheduling this would// mean advancing the next wheel and promoting or executing// those events. So we need to look in that slot too// before proceeding with the rest of this wheel. But we// can't just accept those results outright, we need to// check the best result there against the next slot on// this wheel.if (slot_index == 0 && level < MAX_LEVEL) {// Exception: If we're in the core wheel, and slot 0 is// not empty, there's no point in looking in the outer wheel.// It's guaranteed that the events actually in slot 0 will be// executed no later than anything in the outer wheel.if (level > 0 || !slots_[level][slot_index].events()) {auto up_slot_index = (now_[level + 1] + 1) & MASK;const auto& slot = slots_[level + 1][up_slot_index];for (auto event = slot.events(); event != NULL;event = event->next_) {min = std::min(min, event->scheduled_at() - now);}}}bool found = false;const auto& slot = slots_[level][slot_index];for (auto event = slot.events(); event != NULL;event = event->next_) {min = std::min(min, event->scheduled_at() - now);// In the core wheel all the events in a slot are guaranteed to// run at the same time, so it's enough to just look at the first// one.if (level == 0) {return min;} else {found = true;}}if (found) {return min;}}// Nothing found on this wheel, try the next one (unless the wheel can't// possibly contain an event scheduled earlier than "max").if (level < MAX_LEVEL &&(max >> (WIDTH_BITS * level + 1)) > 0) {return ticks_to_next_event(max, level + 1);}return max;
}#endif // RATAS_TIMER_WHEEL_H
分级时间轮优化普通时间轮定时器相关推荐
- 分级时间轮优化普通时间轮定时器(2):滴答式分层计时轮
<实现较低的计时器粒度以重传TCP(RTO):时间轮算法如何减少开销> <分级时间轮优化普通时间轮定时器> Table of Contents 描述 新闻 用法 执照 资源 其 ...
- java 时间轮算法_时间轮算法(TimingWheel)是如何实现的?
前言 我在2. SOFAJRaft源码分析-JRaft的定时任务调度器是怎么做的?这篇文章里已经讲解过时间轮算法在JRaft中是怎么应用的,但是我感觉我并没有讲解清楚这个东西,导致看了这篇文章依然和没 ...
- STM32输出1-500KHz任意整数频率脉冲,代码时间空间优化实现误差最小频率输出。
提示:此文章只是分析了一种优化STM32发送脉冲减少误差的方法实现,由于本人水平有限,该方法并不是最优解,但确是一种比较容易理解的实现方法. STM32输出1-500KHz任意整数频率脉冲,代码时间空 ...
- 高性能流媒体服务器EasyDSS前端重构(二) webpack + vue + AdminLTE 多页面提取共用文件, 优化编译时间...
本文围绕着实现EasyDSS高性能流媒体服务器的前端框架来展开的,具体EasyDSS的相关信息可在:www.easydss.com 找到! 接上回 <高性能流媒体服务器EasyDSS前端重构(一 ...
- linux上点时间延时,Linux上时间和定时器
Linux下时间和定时器 http://blog.chinaunix.net/u1/35065/showart_1870601.html重点读了第三种方法.文章写得很好,加了一点点注释可参考http: ...
- 【Flink】Flink 1.13 Flink SQL 新特性 性能优化 时区 时间 纠正
文章目录 1.概述 2.window TVF 2.2 GROUPING 2.3 window TopN 2.4 RollUP 2.5 优化 3.时间 3.1 时间函数纠正 3.2 时间类型的使用 3. ...
- vue elementUI 时间控件优化 选择起始时间不能在结束时间之后,结束时间不能在起始时间之前
时间控件优化 elementUI 选择起始时间不能在结束时间之后,结束时间不能在起始时间之前 <el-form-item label="起始时间:" class=" ...
- 滚动轮播图+滚动轮播图定时器的添加(还原lol首页的轮播图)
滚动轮播图+滚动轮播图定时器的添加(还原lol首页的轮播图) 每一步都有详细的注释: <!DOCTYPE html> <html lang="en"> &l ...
- EasyDSS高性能RTMP、HLS(m3u8)、HTTP-FLV、RTSP流媒体服务器前端源码重构(二)-webpack + vue + AdminLTE 多页面提取共用文件, 优化编译时间
接上篇 在上一篇博客中, 我们白手起家, 从零搭建了 webpack + vue + AdminLTE 多页面脚手架. 代码在这里: easydss-web-src , 我为第一篇博客建立了单独的分支 ...
最新文章
- web login do.php,dologin.php
- 推荐9款优秀的 HTML5 音乐播放器
- python快捷_汇总学习Python必备的42个快捷键,看完收获满满
- Actor并发模型入门
- (王道408考研操作系统)第二章进程管理-第二节3:调度算法详解1(FCFS、SJF和HRRN)
- DTC精彩回顾—金学东:从可迁到好迁:人大金仓打造国产数据库生态 助力企业国产化转型...
- POJ 2728 Desert King(最优比率生成树)
- linux amd显卡调风扇转速,从调软件到改BIOS 显卡帝玩转风扇转速
- contextcapture多区块点云_基于ContextCapture倾斜摄影三维建模及其精度分析
- 如何更改文件夹图标和颜色
- Linux下压缩与解压缩
- 新房装修选怎中式装修是不是能省钱
- 全 3D 社交网络 Beloola 测试视频
- 投稿论文图片分辨率达不到要求的解决方案
- linux load averages 和 cpu 使用率
- 1----sim模块的使用
- 王道考研计算机网络学习心得——第一章-计算机网络体系结构
- delphiXE关于线程和多线程、线程的同步与异步执行
- Linux之——UltraISO写入引导扇区时弹出“找到多于1个分区”解决方法
- 第006话 皮皮和月亮石!