Netty中的时间轮

2022-06-02 14:03:48 浏览数 (1)

时间轮是一种可以执行定时任务的数据结构和算法.这篇文章,讲解一下它在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点的时刻小,才会执行这样的任务.

0 人点赞