Netty定时任务调度器-HashedWheelTimer

本文详细解析了Netty的定时任务调度器HashedWheelTimer的工作机制,包括其基本数据结构如Worker、HashedWheelTimeout和HashedWheelBucket,以及如何初始化时间轮、添加定时任务和启动时间轮转的过程。重点介绍了时间轮的ticksPerWheel标准化、添加定时任务的处理以及时间轮的转动和任务执行策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Netty定时任务调度器-HashedWheelTimer

前段时间被问起定时器的设计,之前有使用过一些定时任务中间件,本文以Netty的HashedWheelTimer为例,阐述实现定时任务队列的基本思路。

1、基本数据结构

这里写图片描述

如其命名:HashedWheelTimer一样,HashedWheelTimer通过一定的hash规则将不同timeout的定时任务划分到HashedWheelBucket进行管理,而HashedWheelBucket利用双向链表结构维护了某一时刻需要执行的定时任务列表。

1.1 Worker

Worker继承自Runnable,HashedWheelTimer必须通过Worker线程操作HashedWheelTimer中的定时任务。Worker是整个HashedWheelTimer的执行流程管理者,控制了定时任务分配、全局deadline时间计算、管理未执行的定时任务、时钟计算、未执行定时任务回收处理。

1.2 HashedWheelTimeout

HashedWheelTimeout为HashedWheelTimer的执行单位,维护了其所属的HashedWheelTimer和HashedWheelBucket的引用、需要执行的任务逻辑、当前轮次以及当前任务的超时时间(不变)等,可以认为是自定义任务的一层Wrapper。

1.3 HashedWheelBucket

HashedWheelBucket维护了hash到其内的所有HashedWheelTimeout结构,是一个双向队列。

时间轮每一段格子代表一段时间,如1s,2s,…。以1s为例,当前时间轮可表示delayTimeout在[ns, 8ns]内的时刻。n表示需要转n次时间轮,n储存在每个各自对应的定时任务链表节点信息中,每过一轮该值减1。

根据任务延迟执行的时间对TimerWheel的ticksPerWheel值取模,取模算法采用java8中HashMap的hash算法思想,如:

2. HashdWheelTimer是如何工作的?

首先我们看下HashWheelTimer的构造函数都干了些啥。

  • 初始化时间轮
  • 创建Worker线程(一个HashedWheelTimer仅有一个Worker线程)
  • 开启内存泄露检测

2.1 初始化时间轮HashedWheelTimer

初始化时间轮(HashedWheelTimer)时需要根据构造函数传入的ticksPerWheel对一轮的bucket数量做标准化:

private static int normalizeTicksPerWheel(int ticksPerWheel) {
    int normalizedTicksPerWheel = 1;
    while (normalizedTicksPerWheel < ticksPerWheel) {
        normalizedTicksPerWheel <<= 1;
    }
    return normalizedTicksPerWheel;
}

可以看到是从1逐步试探无符号左移1位来获取比ticksPerWheel大的最小数值,其实这里做了假设:ticksPerWheel不会很大。否则左移次数过多,执行效率低。Java8的HashMap的做法可以借鉴:

/**
* 理论基础:当length总是2的n次方时,h&(length-1)运算等价于对length取模,也就是 
* h%length,但是&比%具有更高的效率。
*/
private int normalizeTicksPerWheel(int ticksPerWheel) {
    int n = ticksPerWheel - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= 1073741824) ? 1073741824 : n + 1;
}

2.2 添加定时任务HashedWheelTimeout

时间轮已经有了,但我们还没有添加定时任务。此时如果调用start()方法启动时间轮,会发生空转浪费资源的情况(基于netty-all-4.1.6.Final),没明白提供公共方法start()的用意。实际在调用newTimeout方法生成并添加定时任务,就会调用start()方法启动时间轮,这种”有需要再调用”的思路很合理!

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

    // Add the timeout to the timeout queue which will be processed on the next tick.
    // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    timeouts.add(timeout);
    return timeout;
}

public void start() {
        switch (WORKER_STATE_UPDATER.get(this)) {
            case WORKER_STATE_INIT:
                if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) {
                    workerThread.start();
                }
                break;
            case WORKER_STATE_STARTED:
                break;
            case WORKER_STATE_SHUTDOWN:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }

        // Wait until the startTime is initialized by the worker.
        while (startTime == 0) { // 2
            try {
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
    }

1,2处的代码会等待worker线程初始化完startTime后才执行后续的HashedWheelTimeout初始化工作,而在Worker执行时通过startTimeInitialized变量完成workerThread和timer线程间的通信,否则startTime将因没有被第一个加入的timeout初始化为当前系统时间影响后续时刻计算。

2.3 开始时间轮转

时刻计算、轮转以及任务执行均由Worker线程负责。

  • 计算全局deadline
  • 处理被取消的定时任务
  • 从HashedWheelTimer维护的timeout列表中取出1W条纪录,计算出remainingRounds并放置到对应的bucket里
  • 执行任务
// waitForNextTick()
long deadline = tickDuration * (tick + 1);
            for (;;) {
                final long currentTime = System.nanoTime() - startTime;
                long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

                if (sleepTimeMs <= 0) {
                    if (currentTime == Long.MIN_VALUE) {
                        return -Long.MAX_VALUE;
                    } else {
                        return currentTime;
                    }
                }

                // Check if we run on windows, as if thats the case we will need
                // to round the sleepTime as workaround for a bug that only affect
                // the JVM if it runs on windows.
                //
                // See https://github.com/netty/netty/issues/356
                if (PlatformDependent.isWindows()) {
                    sleepTimeMs = sleepTimeMs / 10 * 10;
                }

                try {
                    Thread.sleep(sleepTimeMs);
                } catch (InterruptedException ignored) {
                    if (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_SHUTDOWN) {
                        return Long.MIN_VALUE;
                    }
                }
            }

将deadline(时间轮时刻)和当前时间对比,如果还没到deadline时刻,则sleep至deadline时刻;如果已经过了deadline时刻,则直接返回当前时间作为deadline,这里可能存在少许时间误差。

算好deadline后,遍历cancelledTimeout并remove。

取出1W条待执行的timeout纪录,并分配到正确的bucket里:

private void transferTimeoutsToBuckets() {
            // transfer only max. 100000 timeouts per tick to prevent a thread to stale the workerThread when it just
            // adds new timeouts in a loop.
            for (int i = 0; i < 100000; i++) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    // all processed
                    break;
                }
                if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
                    // Was cancelled in the meantime.
                    continue;
                }

                long calculated = timeout.deadline / tickDuration;
                timeout.remainingRounds = (calculated - tick) / wheel.length;

                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);
            }
        }

分配好timeout后,根据当前时钟时刻获取bucket并开始执行定时任务:

public void expireTimeouts(long deadline) {
            HashedWheelTimeout timeout = head;

            // process all timeouts
            while (timeout != null) {
                boolean remove = false;
                if (timeout.remainingRounds <= 0) {
                    if (timeout.deadline <= deadline) {
                        timeout.expire();
                    } else {
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                    remove = true;
                } else if (timeout.isCancelled()) {
                    remove = true;
                } else {
                    timeout.remainingRounds --;
                }
                // store reference to next as we may null out timeout.next in the remove block.
                HashedWheelTimeout next = timeout.next;
                if (remove) {
                    remove(timeout);
                }
                timeout = next;
            }
        }

思路很清晰:如果remainingRounds<=0(如果timeout的deadline

关键点

  • 超时计算方式
  • 时钟tick流转并执行bucket下的timeout对应的task时,全局deadline(自增)的概念
  • HashedWheelTimeout.deadline的含义(不变)
  • bucket.expire()仅执行落在当前bucket内的timeout,否则视为异常

引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值