前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty中的时间轮

Netty中的时间轮

作者头像
书唐瑞
发布2022-06-02 14:03:48
7230
发布2022-06-02 14:03:48
举报
文章被收录于专栏:Netty历险记

时间轮是一种可以执行定时任务的数据结构和算法.这篇文章,讲解一下它在Netty 3.x系列中如何实现的,它在4.x系列将在后面的文章中讲解.

此次讲解的版本如下

代码语言:javascript
复制
<dependency>
    <groupId>org.jboss.netty</groupId>
    <artifactId>netty</artifactId>
    <version>3.2.5.Final</version>
</dependency>

它的实现上是一个一维数组,但把它当成一个环形数组使用.如同时间的车轮一样.

首先说明一点的是,时间轮是按照顺时针的方向'转动',也就是按照顺时针的方向执行时间轮上的任务.

每一个格子的时间跨度都是tickDuration,就是说图中每个小格子的时间长度都是tickDuration.

时间轮在开始'转动'的时候会记录下开始时间startTime.每个格子表示一个tick值,第一个格子的tick值等于1,第二个格子的tick值等于2,以此类推.

如图所示,第9个格子与第1个格子是同一个,第10个格子与第2个是同一个,以此类推.

根据上面的几点说明,计算图中K点的时刻

如果tick=1,那么结果等于startTime+tickDuration * 1

如果tick=9,那么结果等于startTime+tickDuration * 9

想想是不是,因为每个格子的时间跨度都是tickDuration,所以经过n个格子,就是tickDuration * n这么长时间,又由于起始时间是startTime,所以每个格子的结束时刻(例如图中K点,Q点)就等于startTime+tickDuration * n

时间轮初始化之后,它的结构如下图

假如此时时间轮正在执行下图中S格子中的任务,这时向时间轮中添加一个延时delay的任务,时间轮会根据当前所处的位置和时刻,计算新添加的任务应该放在哪个格子位置.而且新添加的任务的deadline=currentTime + delay.也就是说,当前时间加上dalay延时时间就是这个任务要在那个时刻被执行.

时间轮会绑定一个唯一的线程,执行时间轮上的任务.

代码语言:javascript
复制
// 构造器
public HashedWheelTimer(
            ThreadFactory threadFactory,
            long tickDuration, TimeUnit unit, int ticksPerWheel) {

  wheel = createWheel(ticksPerWheel);// 创建时间轮,实际就是一个Set数组
  iterators = createIterators(wheel);// 创建每个Set元素的迭代器
  mask = wheel.length - 1;

  this.tickDuration = tickDuration = unit.toMillis(tickDuration);// 每个格子的时间跨度

  roundDuration = tickDuration * wheel.length; // 转一圈的时间

  // 执行时间轮里面任务的线程
  workerThread = threadFactory.newThread(new ThreadRenamingRunnable(worker, "Hashed wheel timer #" + id.incrementAndGet()));

}
代码语言:javascript
复制
// 源码位置: org.jboss.netty.util.HashedWheelTimer.Worker#run
public void run() {
  List<HashedWheelTimeout> expiredTimeouts = new ArrayList<HashedWheelTimeout>();
  // 记录时间轮启动的开始时间
  startTime = System.currentTimeMillis();
  // 从tick=1的位置开始执行
  tick = 1;
  
  while (!shutdown.get()) {
    // 等到下一个tick的时间到达
    final long deadline = waitForNextTick();
    if (deadline > 0) {
      // 获取到期的任务
      fetchExpiredTimeouts(expiredTimeouts, deadline);
      // 执行到期的任务
      notifyExpiredTimeouts(expiredTimeouts);
    }
  }
}

执行线程会一直等到时间到达当前tick的结束时间.例如之前上面说的K点或Q点位置.

代码语言:javascript
复制
private long waitForNextTick() {
  // 计算第tick格子的结束时刻
  long deadline = startTime + tickDuration * tick;

  for (;;) {
    final long currentTime = System.currentTimeMillis();
    // 可以等价于startTime + tickDuration * tick - currentTime
    final long sleepTime = tickDuration * tick - (currentTime - startTime);
    // 根据上面的等式,它的意思就是说必须等到时间走到了当前格子的结束时刻才终止.
    if (sleepTime <= 0) {
      break;
    }

    try {
        // 如果时间还没有走到tick格子的结束时刻则sleep
      Thread.sleep(sleepTime);
    } catch (InterruptedException e) {
      if (shutdown.get()) {
        return -1;
      }
    }
  }
    // tick加1
  tick ++;
  return deadline;
}

通过图形的方式解释下上面的方法

首先会计算出当前tick的deadline的值,就是图中标注的.如果当前时间(currentTime)还没有走到deadline,那么线程睡眠(deadline-currentTime)这么久,'醒来'之后继续检查.只有当前时间>=deadline的时候,才会跳出循环,开始执行任务.

接下来将当前格子中的所有任务遍历一遍,找出任务的deadline(每个任务在放入时间轮的时候,都会有一个deadline值)比图中deadline小的任务,把它们放入一个集合,然后执行它们.

代码语言:javascript
复制
// 这个方法都在处理同一个格子里面的任务
private void fetchExpiredTimeouts(
                List<HashedWheelTimeout> expiredTimeouts,
                ReusableIterator<HashedWheelTimeout> i, long deadline) {

  List<HashedWheelTimeout> slipped = null;
  i.rewind();
  // 每个格子的底层都是通过HashMap存储任务的,而且每个格子都有一个迭代器,用于迭代HashMap中的任务.
  while (i.hasNext()) {
    HashedWheelTimeout timeout = i.next();
    // 有些任务是在这一圈就要执行,有些任务是要等到第2圈,第3圈...才会执行的.这里只会找出这一圈需要执行的任务
    if (timeout.remainingRounds <= 0) {
      i.remove();
      // 如果任务已经过期,则加入集合中,接下来就会执行这个任务
      if (timeout.deadline <= deadline) {
        expiredTimeouts.add(timeout);
      } else {
        if (slipped == null) {
          slipped = new ArrayList<HashedWheelTimer.HashedWheelTimeout>();
        }
        // 有些任务可能放错了格子,需要重新放入格子
        slipped.add(timeout);
      }
    } else {
      timeout.remainingRounds --;
    }
  }

  if (slipped != null) {
    for (HashedWheelTimeout timeout: slipped) {
      scheduleTimeout(timeout, timeout.deadline - deadline);
    }
  }
}

执行任务

代码语言:javascript
复制
private void notifyExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {
  // 遍历列表,执行任务
  for (int i = expiredTimeouts.size() - 1; i >= 0; i --) {
    expiredTimeouts.get(i).expire();
  }

  expiredTimeouts.clear();
}

简单做个总结,如下图,时间轮在执行的过程,假如当前正在准备执行tick=2的格子中的任务,如果当前时间没有走到deadline时刻,那么线程睡眠,直到时间到达deadline时刻,那么就开始执行格子中的任务(每个格子中的任务都是外部线程提交到时间轮里的).

首先需要筛选出格子中任务的deadline<=图中的deadline.只有任务的deadline<=图中标记的deadline,才会执行这些满足条件的任务.未满足条件的任务只能等到下一圈,或者下下圈才能被执行,这是根据任务的延时时间决定的.

任务在提交到时间轮的时候,它何时被执行,已经被确定了.

假如正在执行S格子中的任务,这个时候添加了一个任务task,它要在D这个时刻被执行,那么当时间走到N这个deadline点的时候,任务task就一定要被执行.

一个任务要在延时300ms的时候被执行,但是未必一定是在延时300ms后被执行.只有时间轮的时刻走到了类似图中N点这样的位置的时候,才会执行当前格子(也就是当前tick)中比N点时刻小的任务,就是任务的deadline的时刻比N点的时刻小,才会执行这样的任务.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Netty历险记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档