Netty-HashedWheelTimer-源码学习

孤人 提交于 2020-08-10 05:26:14

未完待续

引入:

  1. 详解了时间轮的用法: https://www.zhihu.com/question/361018720/answer/937510941
  2. 时间轮的来由【10w定时任务,如何高效触发超时】: https://blog.csdn.net/qijiqiguai/article/details/78606877

1. 为啥需要它?

需求:最近要搞IM,里面有个需求,15秒不发消息,判断用户下线。

经过探讨总共得到两种方法:

  1. 对个每个用户开启一个定时器,15秒后启动它,并判断用户是否下线。
    很显然,这不现实,每个定时器都需要一个线程
  2. 将每个用户最后的请求时间戳缓存下来,然后开启一个定时器,轮询每个用户,判断其是否超时。
    仅启动一个定时器,非常nice,但是问题是需要轮询每个用户,也许应该用多个定时器来处理,但是这都无法改变一个问题,需要轮询大量无关用户,有没有办法解决这个问题?

这个两个方法引出了核心需求: 我希望定时器每次启动都只对需要操作的对象操作。即对可能已经失效的用户来操作。

这个需求的核心问题在于按照时间对任务排序

那为什么要用所谓的时间轮结构?普通队列它不香吗?或者说HashedWheelTimer和java.util.Timer的差别在哪里?

1. 从性能上考虑,(主要体现在插入任务的过程)
明显HashedWheelTimer占优。

  1. HashedWheelTimer插入是直接插入(简称:直插)。
  2. java.util.Timer的插入是上锁,寻找插入点、插入,解锁。

2. 从执行任务的精准性来讲: _ java.util.Timer明显要更准_

  1. java.util.Timer 直接sleep到指定时间唤醒
  2. HashedWheelTimer 需要一格一格的启动停止(导致延后一格,宁晚不要早)

3. 从额外损耗cpu来讲
_ java.util.Timer更有优势_

  1. sleep到需要的启动的时刻启动。
  2. HashedWheelTimer每一个格子都要执行一次。

综合考虑:

  1. 大并发量的任务,且对时间要求不敏感(晚100ms执行也无所谓的任务)。更好的选择是采用HashedWheelTimer
  2. 少量任务,更好的选择是java.util.Timer

2. HashedWheelTimer 图解实现步骤

思路:

  1. 作为一个定时器,我只需要关注它的线程模型(怎么去执行task),怎么添加task,怎么取消task即可。
  2. 我不可能一辈子都记住源码的细节,所以只能抓住它的精髓,它最本质的模型,
  3. 记忆最好的方式是去记住它的模型,用图来画出模型来,才是真正理解记忆。

整体图:

2.1 如何实现一个所谓的时间轮

1) 啥叫时间轮呀?

所谓时间轮,就是按照一个固定的时间刻度,周而复始的移动指向刻度的指向,可以理解为时钟的秒针。

2) 如何周而复始?

正常js程序员首先反应的通过 % 来实现,但是作为大佬,用正常人一眼看出来的东西,太丢人了,
所以大佬选中用 & 来实现(ps:大佬就是大佬,为了一丢丢性能,搞出几十行代码)

3) 如何&来实现呀?

%的实现本质上是基于10进制,与一个数与N的余数,
而&想得到余数,可以让这个数&(2^N - 1) 即可。

4) 这样装逼会有啥缺点?

缺点很明显,想要通过&(2^N - 1) 让时间轮上每个刻度周而复始,
就要求时间轮刻度的总数必须是2^N。
这告诉我们,装逼是要付出代价的,但大佬装逼不用

2.2 为啥Task不直接怼进时间轮,而是通过一个队列怼进去呢?

我想了很久,陡然想起,transferTimeoutsToBuckets里面那个没有用常量的100000。 我猜测大佬的想法可能是基于两点:

  1. 时间轮执行顺序
  2. 代码在线程安全上结构清晰

1)时间轮执行顺序如何体现?

插入Task的时候,将Task插入到正在运行的Bucket后,Bucket正好跳过去了。
这意味着这个Task可能要等一轮的Buckets时间过去才能执行。

2) 代码在线程安全上结构清晰

这是我对这个整个操作最大的认可
唯一的并发点就在存取队列那块,其他的都是单线程跑,代码结构非常清晰。

我觉得这种结构非常值得我们学习,通过队列来有效的拆分复杂的多线程,
使其并发安全集中于某个小点,而不是整个代码每行都需要防止线程安全问题

3. HashedWheelTimer之迷惑行为

3.1 HashedWheelTimer#waitForNextTick 迷之sleep

很久之前看源码是这么写的:

为什么要这么写呢?
原因是sleep在小于10ms的时候,系统中断太高了。
所以作者直接让sleep小于10ms时,sleep(0),这样线程实际上没有休眠,和Thread.yield类似,也就没有中断了。

然而最近的更新

原因是:当sleep(0)时,主机会疯狂执行waitForNextTick(),导致cpu 100%

对此我真的醉了,这一段逻辑压根就没有意义了,应该从根本上解决问题,限制无限出现10ms的情况。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!