线程模型概述
线程模型确定了代码的执行方式
基本的线程池化模式可以描述为:
- 从池的空闲线程列表中选择一个 Thread ,并且指派它去运行一个已提交的任务(一个
Runnable
的实现)。 - 当任务完成时,将该 Thread 返回给该列表,使其可被重用。
虽然池化和重用线程相对于简单地为每个任务都创建和销毁线程是一种进步,但是它并不能消除由上下文切换所带来的开销,其将随着线程数量的增加很快变得明显,并且在高负载下愈演愈烈。此外,仅仅由于应用程序的整体复杂性或者并发需求,在项目的生命周期内也可能会出现其他和线程相关的问题。
EventLoop 接口
运行任务来处理在连接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造通常被称为事件循环。而在 Netty 中使用 io.netty.channel.EventLoop
来实现。
事件循环的基本思想:
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 的事件进行交互,扩展了这些接口/类。
EventLoop 的类层次结构:
在这个模型中,一个 EventLoop 将由一个永远都不会改变的 Thread 驱动,同时认为(Runnable
或者 Callable
)可以直接提交给 EventLoop 实现,以立即执行或者调度执行。根据配置和可用核心的不同可能会创建多个 EventLoop 实例用以优化资源的使用,并且单个 EventLoop 可能会被指派用于服务多个 Channel 。
需要注意的是,Netty 的 EventLoop 在继承了 ScheduledExecutorService
的同事,只定义了一个 parent()
方法。这个方法用于返回当前 EventLoop 实现的实例所属的 EventLoopGroup 的引用。
任务调度
使用 EventLoop
调度任务:
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop()
// 创建一个 Runnable 以供调度稍后执行
.schedule(new Runnable() {
@Override
public void run() {
// 要执行的代码
System.out.println("60 seconds later");
}
// 调度任务在从现在开始的 60 秒之后执行
}, 60, TimeUnit.SECONDS);
使用 EventLoop
调度周期性的任务:
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop()
// 创建一个 Runnable 以供调度稍后执行
.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// 要执行的代码
System.out.println("60 seconds later");
}
// 调度任务在从现在开始的 60 秒之后,以每 60 秒间隔循环执行
}, 60, 60, TimeUnit.SECONDS);
使用 ScheduledFuture
取消任务:
// 调度任务,并获得返回的 ScheduledFuture
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...);
boolean mayInterruptIfRunning = false;
// 取消该任务,防止它再次运行
future.cancel(mayInterruptIfRunning);
线程管理
Netty 线程模型的卓越性能取决于对于当前执行的 Thread 的身份的确认,也就是说,确定它是否是分配给当前 Channel 以及它的 EventLoop 的那个线程(EventLoop 将负责处理一个 Channel 的整个生命周期内的所有事件)。
如果当前调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被直接执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件。每个 EventLoop 都有它自己的任务队列,独立于任何其他的 EventLoop 。
EventLoop 的执行逻辑:
注意:永远不要将一个长时间运行的任务放入执行队列中,以为它将阻塞需要在同一线程上执行的任何其他任务。如果必须要进行阻塞调用或者执行长时间运行的任务,建议使用一个专门的 EventExecutor 。
EventLoop 线程的分配
服务于 Channel 的 I/O 和事件的 EventLoop 包含在 EventLoopGroup 中。根据不同的传输实现,EventLoop 的创建和分配方式也不同。
异步传输
异步传输实现只使用少量的 EventLoop(以及和它们相关联的 Thread),而且在当前的线程模型中,它们可能会被多个 Channel 共享。这样就可以让少量的 Thread 来支撑大量的 Channel ,而不是每个 Channel 分配一个 Thread 。
用于非阻塞传输(NIO、AIO)的 EventLoop 分配方式:
阻塞传输
用于像 OIO(旧的阻塞 I/O)这样的其他传输的设计略有不同,每个 Channel 都将被分配给一个 EventLoop(以及它的 Thread)。
阻塞传输(OIO)的 EventLoop 分配方式: