Netty学习摘记 —— 再谈EventLoop 和线程模型
本文参考
本篇文章是对《Netty In Action》一书第七章"EventLoop和线程模型"的学习摘记,主要内容为线程模型的概述、事件循环的概念和实现、任务调度和实现细节
线程模型概述
线程模型指定了操作系统、编程语言、框架或者应用程序的上下文中的线程管理的关键方面。可见,线程模型确定了代码的执行方式,如何以及何时创建线程将对应用程序代码的执行产生显著的影响,因此开发人员需要理解与权衡不同的模型
在早期的 Java 语言中,我们使用多线程处理的主要方式无非是按需创建和启动新的 Thread 来执行并发的任务单元 —— 一种在高负载下工作得很差的原始方式。Java 5 随后引入了 Executor API(Java 的并发 API —— java.util.concurrent),其线程池通过缓存和重用 Thread极大地提高了性能,基本的线程池化模式可以描述为:
从池的空闲线程列表中选择一个 Thread,并且指派它去运行一个已提交的任务(一个 Runnable的实现);
当任务完成时,将该Thread返回给该列表,使其可被重用
java.util.concurrent
public interface ExecutorAn object that executes submitted Runnable tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads.
虽然池化和重用线程相对于简单地为每个任务都创建和销毁线程是一种进步,但是它并不能消除由上下文切换所带来的开销,它的性能瓶颈随着线程数量的增加和负载的增大很快变得明显
EventLoop接口
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造通常被称为事件循环 —— 一个 Netty 使用了 interface io.netty.channel. EventLoop来适配的术语
在前面的"深入理解Netty核心组件"的文章中,我们提供了下面这张图:
图中标注的④、⑤、⑥和⑦便体现了事件循环的基本思想,在Netty中使用了AtomicBoolean类型的变量wakenUp作为超时标记
Boolean that controls determines if a blocked Selector.select should break out of its selection process. In our case we use a timeout for the select method and the select method will block for that time unless waken up.
事件循环中执行任务的一个极简化的代码如下:
boolean terminated = true;
//...
while (!terminated) {
//阻塞,直到有事件已经就绪可被运行
List<Runnable> readyEvents = blockUntilEventsReady();
for (Runnable ev: readyEvents) {
//循环遍历,并处理所有的事件
ev.run();
}
}
Netty 的 EventLoop采用了两个基本的 API:并发和网络编程
首先,io.netty.util.concurrent 包构建在 JDK 的 java.util.concurrent 包上,用来提供线程执行器
其次,io.netty.channel包中的类,为了与Channel的事件进行交互, 扩展了这些接口/类,下图展示了类与接口的继承和实现关系
事件和任务是以先进先出(FIFO)的顺序执行。这样可以通过保证字节内容总是按正确的顺序被处理,消除潜在的数据损坏的可能性,例如在SingleThreadEventLoop中定义了Queue<Runnable>类型的tailTasks变量来添加需要执行的任务
注意每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。
Netty 4 中的 I/O 和事件处理
由 I/O 操作触发的事件将流经安装了一个或者多个 ChannelHandler的ChannelPipeline。传播这些事件的方法调用可以随后被ChannelHandler所拦截,并且可以按需地处理事件
并且所有的I/O操作和事件都由已经被分配给了 EventLoop的那个Thread来处理
而在以前的版本中所使用的线程模型,只保证了入站(之前称为上游)事件会在所谓的 I/O 线程(对应于 Netty 4 中的EventLoop)中执行。所有的出站(下游)事件都由调用线程处理,其可能是 I/O 线程也可能是别的线程
JDK的任务调度
JDK 提供了 java.util.concurrent 包,它定义了 interface ScheduledExecutorService,下面是对源码的类注释和代码示例
An ExecutorService that can schedule commands to run after a given delay, or to execute periodically.
//创建一个其线程池具有 10 个线程的ScheduledExecutorService
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(
//创建一个 Runnable,以供调度稍后执行
new Runnable() {
@Override
public void run() {
//该任务要打印的消息
System.out.println("Now it is 60 seconds later");
}
//调度任务在从现在开始的 60 秒之后执行
}, 60, TimeUnit.SECONDS);
//...
//一旦调度任务执行完成,就关闭 ScheduledExecutorService 以释放资源
executor.shutdown();
虽然ScheduledExecutorService API 是直截了当的,但是在高负载下它将带来性能上 的负担
EventLoop 的任务调度
ScheduledExecutorService 的实现作为线程池管理的一部分,将会有额外的线程创建。如果有大量任务被紧凑地调度,那么这将成为一个瓶颈。Netty 通过Channel的EventLoop实现任务调度解决了这一问题
Channel ch = CHANNEL_FROM_SOMEWHERE;
ScheduledFuture<?> future = ch.eventLoop().schedule(
//创建一个 Runnable以供调度稍后执行
new Runnable() {
@Override
public void run() {
//要执行的代码
System.out.println("60 seconds later");
}
//调度任务在从现在开始的 60 秒之后执行
}, 60, TimeUnit.SECONDS);
经过 60 秒之后,Runnable实例将由分配给Channel的EventLoop执行
如果要调度任务以每隔 60 秒执行一次,则使用scheduleAtFixedRate()方法
Creates and executes a periodic action that becomes enabled first after the given initial delay, and subsequently with the given period; that is executions will commence after initialDelay then initialDelay+period, then initialDelay + 2 * period, and so on. If any execution of the task encounters an exception, subsequent executions are suppressed. Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.
Channel ch = CHANNEL_FROM_SOMEWHERE;
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
//创建一个 Runnable,以供调度稍后执行
new Runnable() {
@Override
public void run() {
//这将一直运行,直到 ScheduledFuture 被取消
System.out.println("Run every 60 seconds");
}
//调度在 60 秒之后,并且以后每间隔 60 秒运行
}, 60, 60, TimeUnit.SECONDS);
要想取消或者检查(被调度任务的)执行状态,可以使用每个异步操作所返回的 Scheduled- Future
Channel ch = CHANNEL_FROM_SOMEWHERE;
//调度任务,并获得所返回的ScheduledFuture
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println("Run every 60 seconds");
}
}, 60, 60, TimeUnit.SECONDS);
// Some other code that runs...
boolean mayInterruptIfRunning = false;
//取消该任务,防止它再次运行
future.cancel(mayInterruptIfRunning);
Netty的EventLoop扩展了ScheduledExecutorService,所以它提供了使用JDK实现可用的所有方法,包括在前面的示例中使用到的schedule()和scheduleAtFixedRate()方法。所有操作的完整列表可以在ScheduledExecutorService的 Javadoc中找到
线程管理
Netty线程模型能够对当前执行的Thread的身份的进行确定,我们可以在SingleThreadEventExecutor类中找到这个方法
@Override
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}
如果(当前)调用线程正是支撑EventLoop的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件
异步传输的线程分配
异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread), 而且在当前的线程模型中,它们可能会被多个Channel所共享。这使得可以通过尽可能少量的Thread来支撑大量的Channel,而不是每个Channel分配一个Thread
EventLoopGroup负责为每个新创建的Channel分配一个EventLoop。在当前实现中, 使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的 EventLoop 可能会被分配给多个Channel,所以对于所有相关联的 Channel 来说, ThreadLocal都将是一样的
一旦一个 Channel 被分配给一个 EventLoop,它将在它的整个生命周期中都使用这个 EventLoop(以及相关联的Thread)
阻塞传输的线程分配
每一个Channel都将被分配给一个EventLoop(以及它的Thread)
每个 Channel 的 I/O 事件仍然都将只会被一个 Thread (用于支撑该Channel的EventLoop的那个Thread)处理
这种相似的设计模式使得我们能够方便的在nio和oio之间切换,而不用对我们的业务逻辑代码进行大的改动