Netty技术细节源码分析-HashedWheelTimer时间轮原理分析

本文是该篇的修正版
本文的github地址:点此

该文所涉及的netty源码版本为4.1.6。

Netty时间轮HashedWheelTimer是什么

Netty的时间轮HashedWheelTimer给出了一个粗略的定时器实现,之所以称之为粗略的实现是因为该时间轮并没有严格的准时执行定时任务,而是在每隔一个时间间隔之后的时间节点执行,并执行当前时间节点之前到期的定时任务。当然具体的定时任务的时间执行精度可以通过调节HashedWheelTimer构造方法的时间间隔的大小来进行调节,在大多数网络应用的情况下,由于IO延迟的存在,并不会严格要求具体的时间执行精度,所以默认的100ms时间间隔可以满足大多数的情况,不需要再花精力去调节该时间精度。

HashedWheelTimer的实现原理

HashedWheelTimer内部的数据结构

	private final HashedWheelBucket[] wheel;

HashedWheelTimer的主体数据结构wheel是一个由多个链表所组成的数组,默认情况下该数组的大小为512。当定时任务准备加入到时间轮中的时候,将会以其等待执行的时间为依据选择该数组上的一个具体槽位上的链表加入。

	private HashedWheelTimeout head;
        private HashedWheelTimeout tail;

在这个wheel数组中,每一个槽位都是一条由HashedWheelTimeout所组成的链表,其中链表中的每一个节点都是一个等待执行的定时任务。

HashedWheelTimer内部的线程模型

在HashedWheelTimer中,其内部是一个单线程的worker线程,通过类似eventloop的工作模式进行定时任务的调度。

@Override
        public void run() {
            // Initialize the startTime.
            startTime = System.nanoTime();
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            // Notify the other threads waiting for the initialization at start().
            startTimeInitialized.countDown();

            do {
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    transferTimeoutsToBuckets();
                    HashedWheelBucket bucket =
                            wheel[(int) (tick & mask)];
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            // Fill the unprocessedTimeouts so we can return them from stop() method.
            for (HashedWheelBucket bucket: wheel) {
                bucket.clearTimeouts(unprocessedTimeouts);
            }
            for (;;) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                unprocessedTimeouts.add(timeout);
            }
        }

简单看到HashedWheelTimer内部的woker线程的run()方法,在其首先会记录启动时间作为startTime作为接下来调度定时任务的时间依据,而之后会通过CountDownLatch来通知所有外部线程当前worker工作线程已经初始化完毕。之后的循环体便是当时间轮持续生效的时间里的具体调度逻辑。时间刻度是时间轮的一个重要属性,其默认为100ms,此处的循环间隔便是时间轮的时间刻度,默认情况下就是间隔100ms进行一次调度循环。工作线程会维护当前工作线程具体循环了多少轮,用于定位具体执行触发时间轮数组上的哪一个位置上的链表。当时间轮准备shutdown的阶段,最后的代码会对未执行的任务整理到未执行的队列中。
由此可见,worker线程的run()方法中基本定义了工作线程的整个生命周期,从初始的初始化到循环体中的具体调度,最后到未执行任务的具体清理。整体的调度逻辑便主要在这里执行。值得注意的是,在这里的前提下,每个HashedWheelTimer时间轮都会有一个工作线程进行调度,所以不需要在netty中在每一个连接中单独使用一个HashedWheelTimer来进行定时任务的调度,否则可能将对性能产生影响。

向HashedWheelTimer加入一个定时任务的流程

当调用HashedWheelTimer的newTimeout()方法的时候,即是将定时任务加入时间轮中的api。

    @Override
    public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }
        start();

        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

当此次是首次向该时间轮加入定时任务的时候,将会通过start()方法开始执行上文所述的worker工作线程的启动与循环调度逻辑,这里暂且不提。之后计算定时任务触发时间相对于时间轮初始化时间的相对时间间隔deadline,并将其包装为一个链表节点HashedWheelTimeout ,投入到timeouts队列中,等待worker工作线程在下一轮调度循环中将其加入到时间轮的具体链表中等待触发执行,timeouts的实现是一个mpsc队列,关于mpsc队列可以查看此文,这里也符合多生产者单消费者的队列模型。

HashedWheelTimer中工作线程的具体调度

            do {
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    transferTimeoutsToBuckets();
                    HashedWheelBucket bucket =
                            wheel[(int) (tick & mask)];
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

在HashedWheelTimer中的工作线程run()方法的主要循环中,主要分为三个步骤。

  • 首先worker线程会通过waitForNextTick()方法根据时间轮的时间刻度等待一轮循环的开始,在默认情况下时间轮的时间刻度是100ms,那么此处worker线程也将在这个方法中sleep相应的时间等待下一轮循环的开始。此处也决定了时间轮的定时任务时间精度。
  • 当worker线程经过相应时间间隔的sleep之后,也代表新的一轮调度开始。此时,会通过transferTimeoutsToBuckets()方法将之前刚刚加入到timeouts队列中的定时任务放入到时间轮具体槽位上的链表中。
            for (int i = 0; i < 100000; i++) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED
                        || !timeout.compareAndSetState(HashedWheelTimeout.ST_INIT, HashedWheelTimeout.ST_IN_BUCKET)) {
                    timeout.remove();
                    continue;
                }
                long calculated = timeout.deadline / tickDuration;
                long remainingRounds = (calculated - tick) / wheel.length;
                timeout.remainingRounds = remainingRounds;

                final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
                int stopIndex = (int) (ticks & mask);

                HashedWheelBucket bucket = wheel[stopIndex];
                bucket.addTimeout(timeout);
            }

首先,在每一轮的调度中,最多只会从timeouts队列中定位到时间轮100000个定时任务,这也是为了防止在这里耗时过久导致后面触发定时任务的延迟。在这里会不断从timeouts队列中获取刚加入的定时任务。具体的计算流程便是将定时任务相对于时间轮初始化时间的相对间隔与时间轮的时间刻度相除得到相对于初始化时间的具体轮数,之后便在减去当前轮数得到还需要遍历几遍整个时间轮数组得到remainingRounds,最后将轮数与时间轮数组长度-1相与,得到该定时任务到底应该存放到时间轮上哪个位置的链表。用具体的数组举个例子,该时间轮初始化时间为12点,时间刻度为1小时,时间轮数组长度为8,当前时间13点,当向时间轮加入一个明天13点执行的任务的时候,首先得到该任务相对于初始化的时间间隔是25小时,也就是需要25轮调度,而当前13点,当前调度轮数为1,因此还需要24轮调度,就需要再遍历3轮时间轮,因此remainingRounds为3,再根据25与8-1相与的结果为1,因此将该定时任务放置到时间轮数组下标为1的链表上等待被触发。这便是一次完整的定时任务加入到时间轮具体位置的计算。

  • 在worker线程的最后,就需要来具体执行定时任务了,首先通过当前循环轮数与时间轮数组长度-1相与的结果定位具体触发时间轮数组上哪个位置上的链表,再通过expireTimeouts()方法依次对链表上的定时任务进行触发执行。这里的流程就相对很简单,链表上的节点如果remainingRounds小于等于0,那么就可以直接执行这个定时任务,如果remainingRounds大于0,那么显然还没有到达触发的时间点,则将其-1等待下一轮的调度之后再进行执行。在继续回到上面的例子,当14点来临之时,此时工作线程将进行第2轮的调度,将会把2与8-1进行相与得到结果2,那么当前工作线程就会选择时间轮数组下标为2的链表依次判断是否需要触发,如果remainingRounds为0将会直接触发,否则将会将remainingRounds-1等待下一轮的执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值