更新:这是my implementation of Hashed Timing Wheels。如果您有提高性能和并发性的想法,请告诉我。 (20-JAN-2009)
// Sample usage:
public static void main(String[] args) throws Exception {
Timer timer = new HashedWheelTimer();
for (int i = 0; i < 100000; i ++) {
timer.newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// Extend another second.
timeout.extend();
}
}, 1000, TimeUnit.MILLISECONDS);
}
}
更新:我使用Hierarchical and Hashed Timing Wheels解决了这个问题。 (19-JAN-2009)
我正在尝试在Java中实现一个特殊用途计时器,它针对超时处理进行了优化。例如,用户可以使用死线注册任务,并且计时器可以在死线结束时通知用户的回调方法。在大多数情况下,注册任务将在很短的时间内完成,因此大多数任务将被取消(例如task.cancel())或重新安排到将来(例如task.rescheduleToLater(1,TimeUnit.SECOND))
我想使用此计时器来检测空闲套接字连接(例如,在10秒内没有收到消息时关闭连接)和写入超时(例如,当写操作未在30秒内完成时引发异常。)在在大多数情况下,超时不会发生,客户端将发送消息并且响应将被发送,除非有一个奇怪的网络问题..
我不能使用java.util.Timer或java.util.concurrent.ScheduledThreadPoolExecutor,因为它们假设大多数任务都应该超时。如果任务被取消,则取消的任务将存储在其内部堆中,直到调用ScheduledThreadPoolExecutor.purge(),这是一项非常昂贵的操作。 (也许是O(NlogN)?)
在我在CS类中学到的传统堆或优先级队列中,更新元素的优先级是一项昂贵的操作(在许多情况下是O(logN),因为它只能通过删除元素并重新插入来实现它有一个新的优先级值。像Fibonacci堆的一些堆有O(1)时间的reduceKey()和min()操作,但我至少需要快速的raiseKey()和min()(或reduceKey()和max ())。
您是否知道针对此特定用例进行了高度优化的数据结构?我正在考虑的一个策略是将所有任务存储在哈希表中,并且每隔一秒左右迭代所有任务,但它并不那么漂亮。
答案 0 :(得分:13)
试图将事情快速完成的正常情况与错误案件分开处理?
同时使用哈希表和优先级队列。当一个任务启动时,它会被放入哈希表中,如果它快速完成,它将在O(1)时间内被删除。
每隔一秒扫描哈希表,任何长时间的任务,比如0.75秒,都会被移动到优先级队列。优先级队列应该总是很小并且易于处理。这假设一秒钟远小于您要查找的超时时间。
如果扫描哈希表太慢,您可以使用两个哈希表,基本上一个用于偶数秒,一个用于奇数秒。当任务开始时,它将被放入当前的哈希表中。每秒将所有任务从非当前哈希表移动到优先级队列并交换哈希表,以便当前哈希表现在为空,非当前表包含在一到两秒前开始的任务。
选项比使用优先级队列要复杂得多,但很容易实现,应该是稳定的。
答案 1 :(得分:11)
据我所知(我写了一篇关于新优先级队列的文章,其中也回顾了过去的结果),没有优先级队列实现获得斐波纳契堆的界限,以及恒定时间增加键。
从字面上理解它有一个小问题。如果你可以在O(1)中获得增加键,那么你可以在O(1)中删除 - 只需将键增加到+无穷大(你可以使用一些标准的摊销技巧处理充满大量+无穷大的队列) )。但是如果find-min也是O(1),那意味着delete-min = find-min + delete变为O(1)。在基于比较的优先级队列中这是不可能的,因为排序绑定暗示(插入所有内容,然后逐个删除)
n * insert + n * delete-min&gt; n log n。
这里的要点是,如果您希望优先级队列支持O(1)中的增加键,那么您必须接受以下处罚之一:
但是,据我所知,没有人做过最后一个选择。我一直认为这是在一个非常基本的数据结构领域获得新结果的机会。
答案 2 :(得分:6)
使用Hashed Timing Wheel - Google'哈希分层时间轮'了解更多信息。这是对这里人们所作答案的概括。我更喜欢带有大轮尺寸的散列定时轮到分级定时轮。
答案 3 :(得分:5)
哈希和O(logN)结构的某些组合应该按照你的要求进行。
我很想用你分析问题的方式来狡辩。在上面的评论中,您说
因为更新会非常频繁地发生。假设我们每个连接发送M个消息,那么总时间变为O(MNlogN),这是非常大的。 - Trustin Lee(6小时前)
这是绝对正确的。但是我认识的大多数人都会专注于每条消息的成本,理论上当你的应用程序有越来越多的工作要做时,显然它需要更多的资源。
因此,如果你的应用程序有10亿个套接字同时打开 (这真的可能吗?),每个消息的插入成本只有大约60个。
我敢打赌这是过早的优化:你没有用CodeAnalyst或VTune这样的性能分析工具来测量系统中的瓶颈。
无论如何,一旦你决定没有一个单一的结构可以做你想要的东西,并且你想要不同算法的优点和缺点的组合,可能有无数种方法可以做你所要求的。
一种可能性是将套接字域N划分为一些大小为B的桶,然后将每个套接字散列到其中一个(N / B)桶中。在那个桶中是一个堆(或其他)具有O(log B)更新时间。如果N的上限没有提前修复,但可能会有所不同,那么你可以动态创建更多的桶,这会增加一些复杂性,但肯定是可行的。
在最坏的情况下,看门狗定时器必须搜索(N / B)队列以进行过期,但我假设看门狗定时器不需要以任何特定顺序杀死空闲套接字! 也就是说,如果10个套接字在最后一个时间片中空闲,那么它不必搜索那个超时的域,处理它,然后找到超时的套接字,等等。它只是必须扫描(N / B)一组桶并列举所有超时。
如果您对线性数据桶阵列不满意,可以使用队列的优先级队列,但是您希望避免在每条消息上更新该队列,否则您就会回到起始位置。相反,定义一些小于实际超时的时间。 (比如,3/4或7/8)并且只有当最长时间超过该队列时才将低级队列放入高级队列。
冒着说明显而易见的风险,你不希望你的队列键入已过去的时间。键应该开始时间。对于队列中的每条记录,必须不断更新已用时间,但每条记录的开始时间不会改变。
答案 4 :(得分:3)
有一种非常简单的方法可以在O(1)中执行所有插入和删除,利用以下事实:1)优先级基于时间,2)您可能有一个小的,固定数量的超时持续时间。
如果您有多个不同的超时持续时间,则整个结构的每个操作的复杂度为O(log m)。插入是O(log m),因为需要查找要插入的队列。 Remove-min是O(log m),用于恢复堆。如果您取消队列的头部,则取消为O(1)但最坏情况为O(log m)。因为m是一个小的固定数,所以O(log m)基本上是O(1)。它不会随着任务数量而扩展。
答案 5 :(得分:2)
您的具体方案向我建议了一个循环缓冲区。如果最大超时为30秒,我们想要至少每十分之一秒收一次套接字,然后使用300个双向链表的缓冲区,在此期间每十分之一秒一个。要在条目上“增加时间”,将其从列表中删除,并将其添加到新的第十秒时段(两个常量时间操作)。当一个句号结束时,收获当前列表中遗留的任何内容(可能通过将其提供给收割者线程)并推进当前列表指针。
答案 6 :(得分:0)
您对队列中的项目数量有一个硬限制 - TCP套接字有限制。
因此问题是有限的。我怀疑任何聪明的数据结构都比使用内置类型慢。
答案 7 :(得分:0)
有没有充分的理由不使用java.lang.PriorityQueue?在log(N)时间内不删除()处理取消操作?然后根据队列前面的项目的时间实现自己的等待。
答案 8 :(得分:0)
我认为将所有任务存储在列表中并迭代它们是最好的。
你必须(要)在一台非常强大的机器上运行服务器才能达到这个成本很重要的极限?