转载自:https://blog.csdn.net/u010853261/article/details/62043709
https://blog.csdn.net/u010412719/article/details/78107741
Reactor线程模型有三种
- 单线程模型
- 多线程模型
- 主从Reactor线程模型
关于这三种线程模型的原型,可以参看:https://blog.csdn.net/u010853261/article/details/55805216
前面介绍了Reactor线程模型的原型实现,那么NIOEventLoopGroup是怎么与Reactor关联在一起的呢?其实NIOEventLoopGroup就是一个线程池实现,通过设置不同的NIOEventLoopGroup方式就可以对应三种不同的Reactor线程模型。
这里我只给出服务端的配置,对于客户端都是一样的。
单线程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
.channel(NioServerSocketChannel.class);
//.....
上面实例化了一个NIOEventLoopGroup,构造参数是1表示是单线程的线程池。然后接着调用 b.group(bossGroup)
设置了服务器端的 EventLoopGroup. 当传入一个 group 时, 那么 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup 了.
这时候呢, 因为 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup, 并且这个 NioEventLoopGroup 只有一个线程, 这样就会导致 Netty 中的 acceptor 和后续的所有客户端连接的 IO 操作都是在一个线程中处理的. 那么对应到 Reactor 的线程模型中, 我们这样设置 NioEventLoopGroup 时, 就相当于 Reactor 单线程模型.
多线程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class);
//...
bossGroup 中只有一个线程, 在workerGroup线程池中没有指定线程数量,所以默认是 CPU 核心数乘2, 因此对应的到 Reactor 线程模型中, 这样设置的 NioEventLoopGroup 其实就是 Reactor 多线程模型.
主从Reactor线程模型
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class);
//...
Netty 的服务器端的 acceptor 阶段, 没有使用到多线程, 因此上面的主从多线程模型 在 Netty 的服务器端是不存在的.
服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费。
Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。Netty的多线程编程最佳实践如下:
1)创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO I/O线程。
2)尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
3)解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
4)如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
5)如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。
Netty内部都是通过线程在处理各种数据,EventLoopGroup就是用来管理调度他们的,注册Channel,管理他们的生命周期,下面就来看看EventLoopGroup是怎样工作的(基于4.1.11.Final源码)。
public class NioEventLoopGroup extends MultithreadEventLoopGroup {
/**
* Create a new instance using the default number of threads, the default {@link ThreadFactory} and
* the {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
*/
public NioEventLoopGroup() {
this(0);
}
/**
* Create a new instance using the specified number of threads, {@link ThreadFactory} and the
* {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
*/
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
/**
* Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the
* {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
*/
public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
this(nThreads, threadFactory, SelectorProvider.provider());
}
public NioEventLoopGroup(int nThreads, Executor executor) {
this(nThreads, executor, SelectorProvider.provider());
}
//...略
public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory,
final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
//...略
NioEventLoopGroup会调用其父类MultithreadEventLoopGroup的构造函数:
/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
*/
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
这里,内部线程数DEFAULT_EVENT_LOOP_THREADS
的大小是:
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
MultithreadEventLoopGroup的父类是MultithreadEventExecutorGroup:
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
//...略
}
}
chooser = chooserFactory.newChooser(children);
final FutureListener
children 是EventExecutor数组对象,其大小是 nThreads, 这样就构成了一个线程池,里面存放的是通过NioEventLoopGroup的newChild方法生成的NioEventLoop对象:
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
注意到这里,children中所有的线程公用executor
,SelectorProvider
,SelectStrategyFactory
以及RejectedExecutionHandler
。SelectorProvider是通过SelectorProvider.provider()
获取的,而这个方法返回的是一个单例SelectorProvider。至此,Group和内部的Loop对象以及Executor就创建完毕。
NioEventLoop继承于SingleThreadEventLoop, 而SingleThreadEventLoop又继承于SingleThreadEventExecutor,SingleThreadEventExecutor继承于AbstractScheduledEventExecutor。
SingleThreadEventExecutor是Netty中对本地线程的抽象, 它内部有一个Thread thread属性, 存储了一个本地Java线程。因此可以认为, 一个NioEventLoop其实和一个特定的线程绑定, 并且在其生命周期内, 绑定的线程都不会再改变。
在AbstractScheduledEventExecutor中,Netty 实现了NioEventLoop的schedule功能, 即可以通过调用一个NioEventLoop 实例的schedule方法来运行一些定时任务. 而在SingleThreadEventLoop中, 又实现了任务队列的功能, 通过它, 可以调用一个NioEventLoop实例的execute方法来向任务队列中添加一个 task,并由 NioEventLoop 进行调度执行。
通常来说, NioEventLoop 肩负着两种任务, 第一个是作为IO线程, 执行与 Channel 相关的IO操作, 包括调用 select 等待就绪的IO事件、读写数据与数据的处理等; 而第二个任务是作为任务队列, 执行taskQueue中的任务, 例如用户调用eventLoop.schedule提交的定时任务也是这个线程执行的。
下面是它的具体的继承关系图:
它实现了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,正是因为这种设计,导致NioEventLoop和其父类功能实现非常复杂。
作为NIO框架的Reactor线程,NioEventLoop需要处理网络I/O读写事件,因此它必须聚合一个Selector对象。在NioEventLoop构造时将创建并打开一个新的Selector。Netty对Selector的selectedKeys进行了优化,用户可以通过io.netty.noKeySetOptimization
开关决定是否启用该优化项。默认不打开selectedKeys优化功能。
对于NioEventLoop的实例化,基本就是在NioEventLoopGroup.newChild()
中调用的:
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
unwrappedSelector = selectorTuple.unwrappedSelector;
selectStrategy = strategy;
}
来看NioEventLoop的run:
protected void run() {
/** 死循环:NioEventLoop 事件循环的核心就是这里! */
for (;;) {
try {
// 通过 select/selectNow 调用查询当前是否有就绪的 IO 事件
// 当 selectStrategy.calculateStrategy() 返回的是 CONTINUE, 就结束此轮循环,进入下一轮循环;
// 当返回的是 SELECT, 就表示任务队列为空,就调用select(Boolean);
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}// end switch
// 当有IO事件就绪时, 就会处理这些IO事件
cancelledKeys = 0;
needsToSelectAgain = false;
// ioRatio表示:此线程分配给IO操作所占的时间比(即运行processSelectedKeys耗时在整个循环中所占用的时间).
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
//查询就绪的 IO 事件, 然后处理它;
processSelectedKeys();
} finally {
//运行 taskQueue 中的任务.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
//查询就绪的 IO 事件, 然后处理它;
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
//...略
}
}
上面函数中的一个死循环 for(;;)
就是NioEventLoop事件循环执行机制。下面对上面过程进行详解,分两步:IO事件轮询、IO事件的处理。
1.IO事件轮询
首先, 在run()
方法中, 第一步是调用hasTasks()
方法来判断当前任务队列中是否有任务:
protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty();
}
这个方法很简单, 仅仅是检查了一下 taskQueue 是否为空。至于 taskQueue 是什么呢, 其实它就是存放一系列的需要由此 EventLoop 所执行的任务列表。
1)当 taskQueue 不为空时, hasTasks() 就会返回TRUE,那么selectStrategy.calculateStrategy()
的实现里面就会执行selectSupplier.get()
而get()
方法里面会调用 selectNow();
:
void selectNow() throws IOException {
try {
selector.selectNow();
} finally {
// restore wakup state if needed
if (wakenUp.get()) {
selector.wakeup();
}
}
}
这个 selector 字段正是 Java NIO 中的多路复用器 Selector(比如KQueueSelectorImpl实例)。selector.selectNow()
立即返回当前就绪的IO事件的个数,如果存在IO事件,那么在switch 语句中就会直接执行 default,直接跳出switch语句,如果不存在,就是返回0,对应于continue,忽略此次循环。
2)当taskQueue为空时,selectStrategy.calculateStrategy()
就会返回SelectStrategy.SELECT
,对于switch case语句就是执行select()
函数,阻塞等待IO事件就绪。
selector.select()
是选择一些I/O操作已经准备好的管道。每个管道对应着一个key。这个方法是一个阻塞的选择操作。当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。selector.selectNow()
与selector.select()
的区别在于,是非阻塞的,即当前操作即使没有通道准备好也是立即返回,只是返回的是0,不会阻塞当前线程.
2.IO事件处理
在 NioEventLoop.run()
方法中, 第一步是通过 select/selectNow 调用查询当前是否有就绪的 IO 事件. 那么当有 IO 事件就绪时, 第二步自然就是处理这些 IO 事件啦.首先让我们来看一下 NioEventLoop.run 中循环的剩余部分(核心部分):
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
上面列出的代码中, 有两个关键的调用, 第一个是 processSelectedKeys()
调用, 根据字面意思, 我们可以猜出这个方法肯定是查询就绪的 IO 事件, 然后处理它; 第二个调用是 runAllTasks()
, 这个方法我们也可以一眼就看出来它的功能就是运行 taskQueue 中的任务.
ioRatio表示的是此线程分配给 IO 操作所占的时间比(即运行 processSelectedKeys 耗时在整个循环中所占用的时间). 例如 ioRatio 默认是 50, 则表示 IO 操作和执行 task 的所占用的线程执行时间比是 1 : 1. 当知道了 IO 操作耗时和它所占用的时间比, 那么执行 task 的时间就可以很方便的计算出来了。
当ioRatio为100时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); 而当 ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时), 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 runAllTasks().