欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
Netty框架的主要线程就是I/O线程,线程模型设计的好坏,决定了系统的吞吐量、并发性和安全性等架构质量属性。Netty的线程模型被精心地设计,既提升了框架的并发性能,又能在很大程度避免锁,局部实现了无锁化设计。
1. 线程模型
一般首先会想到的是经典的Reactor线程模型
,尽管不同的NIO框架对于Reactor模式的实现存在差异,但本质上还是遵循了Reactor的基础线程模型。Reactor单线程模型、Reactor多线程模型、主从Reactor多线程模型详见IO系列4-由浅入深Reactor和Proactor模式。这里不在赘述。
1.1 Netty的线程模型
Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线层模型。
下面让我们通过一张原理图来快速了解Netty的线程模型。
可以通过Netty服务端启动代码来了解它的线程模型:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(Channel ch)throws IOException{
ch.pipeline().addLast(new NettyMessageDecoder(1024 * 1024, 4, 4));
ch.pipeline().addLast(new NettyMessageEncoder());
ch.pipeline().addLast("readTimeoutHandler",new ReadTimeoutHandler(50));
ch.pipeline().addLast(new LoginAuthRespHandler());
ch.pipeline().addLast("HeartBeatHandler",new HeartBeatRespHandler());
}
});
// 绑定端口,同步等待成功
b.bind(NettyConstant.REMOTEIP, NettyConstant.PORT).sync();
复制代码
服务端启动的时候,创建了两个NioEventLoopGroup,它们实际是两个独立的Reactor线程池。一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。
-
Netty用于
接收客户端请求的线程池
(bossGroup)职责如下:- 接收客户端TCP连接,初始化Channel参数;
- 将链路状态变更事件通知给ChannelPipeline。
-
Netty
处理I/O操作的Reactor线程池
(workGroup)职责如下:-
异步读取通信对端的数据报,发送读事件到ChannelPipeline;
-
异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;
-
执行系统调用Task;
-
执行定时任务Task,例如链路空闲状态监测定时任务。
-
1.1.1 实现Reactor单线程模型
下⾯直接给出配置的实例:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
.channel(NioServerSocketChannel.class);
//.....
复制代码
上⾯实例化了⼀个NIOEventLoopGroup,构造参数是1表⽰是单线程的线程池。然后接着我们调⽤ b.group(bossGroup) 设置了服务器端的 EventLoopGroup. 有些朋友可能会有疑惑: 我记得在启动服务器端的 Netty 程序时, 是需要设置 bossGroup 和 workerGroup 的, 为什么这⾥就只有⼀个 bossGroup? 其实很简单, ServerBootstrap 重写了 group ⽅法:
@Override
public ServerBootstrap group(EventLoopGroup group) {
return group(group, group);
}
复制代码
因此当传⼊⼀个 group 时, 那么 bossGroup 和 workerGroup 就是同⼀个 NioEventLoopGroup 了. 这时候呢, 因为 bossGroup 和 workerGroup 就是同⼀个 NioEventLoopGroup, 并且这个 NioEventLoopGroup 只有⼀个线程, 这样就会导致 Netty 中的 acceptor 和后续的所有客户端连接的 IO 操作都是在⼀个线程中处理的. 那么对应到 Reactor 的线程模型中, 我们这样 设置 NioEventLoopGroup 时, 就相当于 Reactor 单线程模型.
1.1.2 实现Reactor多线程模型
同理,先给出源码:
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
.channel(NioServerSocketChannel.class);
//...
复制代码
bossGroup 中是多个线程, 因此对应的到 Reactor 线程模型中, 这样设置的 NioEventLoopGroup 其实就是 Reactor 多线程模型.
1.1.3 主从Reactor线程模型
实现⽅式如下:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class);
//...
复制代码
通过调整线程池的线程个数、是否共享线程池等方式,Netty的Reactor线程模型可以在单线程、多线程和主从多线程间切换
,这种灵活的配置方式可以最大程度地满足不同用户的个性化定制。
为了尽可能地提升性能,Netty在很多地方进行了无锁化的设计,例如在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列—多个工作线程的模型性能更优。
它的设计原理如图:
Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead (Object msg)。只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
1.2 最佳实践
Netty的多线程编程最佳实践如下。
(1)创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO I/O线程
。
(2)尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
(3)解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
(4)如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
(5)如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。
2. NioEventLoopGroup实例化过程
下⾯来分析对NioEventLoopGroup类进⾏实例化的过程中发⽣了什么。
NioEventLoopGroup 类层次结构。先给出类图:
对于NioEventLoopGroup核⼼的类继承关系就是:
NioEventLoopGroup –>MultithreadEventLoopGroup –> MultithreadEventExecutorGroup
下⾯从这三个类出发分析NioEventLoopGroup实例化过程。
NioEventLoopGroup实例化过程,⽰意图如下:
下⾯⼤致解释⼀下这个实例化过程做了什么:
2.1 在NioEventLoopGroup构造器调⽤:
如果对于不指定线程数参数的构造器,默认设置为0(但是在后⾯的构造器中会判断,如果设置为0,就会初始化为2* CPU数量);
publicNioEventLoopGroup() {
this(0);
}
复制代码
然后调⽤:
publicNioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
复制代码
这⾥设置了NioEventLoopGroup线程池中每个线程执⾏器默认是null(这⾥设置为null, 在后⾯的构造器中会判断,如果为null就实例化⼀个线程执⾏器)。
再调⽤:
publicNioEventLoopGroup(int nThreads, Executor executor) {
this(nThreads, executor, SelectorProvider.provider());
}
复制代码
这⾥就存在于JDK的NIO的交互了,这⾥设置了线程池的SelectorProvider, 通过
SelectorProvider.provider()
返回。
然后调⽤:
publicNioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider) {
this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
}
复制代码
在这个重载的构造器中⼜传⼊了默认的选择策略⼯⼚DefaultSelectStrategyFactory
;
最后调⽤:
publicNioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
复制代码
这⾥就是调⽤⽗类MultithreadEventLoopGroup的构造器了, 这⾥还添加了线程的拒绝执⾏策略。
2.2 在MultithreadEventLoopGroup构造器调⽤
protectedMultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
复制代码
构造器被定义成protect,表⽰只能在NioEventLoopGroup中被调⽤,⼀定层度上的保护作⽤。这⾥就是对线程数进⾏了判断,nThreads
为0 的时候就设置成 DEFAULT_EVENT_LOOP_THREADS
这个常量。这个常量的定义如下:其实就是在
MultithreadEventLoopGroup的静态代码段
,其实就是将DEFAULT_EVENT_LOOP_THREADS赋值为CPU核⼼数* 2;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
复制代码
接下来就是调⽤的基类MultithreadEventExecutorGroup的构造器。
2.3 MultithreadEventExecutorGroup的构造器:
protectedMultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
复制代码
这个构造器⾥⾯多传⼊了⼀个参数 DefaultEventExecutorChooserFactory.INSTANCE, 通过这个EventLoop选择器⼯⼚可以实例化GenericEventExecutorChooser这个类, 这个类是EventLoopGroup线程池⾥⾯的EventLoop的选择器,调⽤GenericEventExecutorChooser.next()
⽅法可以从线程池中选择出⼀个合适的EventLoop线程。
然后就是重载调⽤MultithreadEventExecutorGroup类的构造器: 构造器调⽤到了这⾥其实也就是不断向上传递调⽤的终点了,由于构造器代码⽐较长,我就删除⼀些校验和不重要的代码,只保留核⼼代码:
/**
* @param nThreads 使用的线程数,默认为 核数*2
* @param executor 执行器,如果传入null,则采用Netty默认的线程工厂和默认的执行器ThreadPerTaskExecutor
* @param chooserFactory 单例DefaultEventExecutorChooserFactory
* @param args 在创建执行器的时候传入固定参数
*/
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
/** 1.初始化线程池
*/
// 参数校验nThread合法性,
checkPositive(nThreads, "nThreads");
//executor校验⾮空, 如果为空就创建ThreadPerTaskExecutor, 该类实现了 Executor接⼝
// 这个executor 是⽤来执⾏线程池中的所有的线程,也就是所有的NioEventLoop,其实从
//NioEventLoop构造器中也可以知道,NioEventLoop构造器中都传⼊了executor这个参数。
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
// 创建指定线程数的执行器数组
//这⾥的children数组,其实就是线程池的核⼼实现,线程池中就是通过指定的线程数组来实现线程池;
//数组中每个元素其实就是⼀个EventLoop,EventLoop是EventExecutor的⼦接⼝。
children = new EventExecutor[nThreads];
// 初始化线程数组
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// todo 创建 new NioEventLoop()
// newChild(executor, args) 函数在NioEventLoopGroup类中实现了,
// 实质就是就是存⼊了⼀个 NIOEventLoop类实例
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 {
// 创建失败,优雅的关闭
if (!success) {
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}
/** 2.实例化线程⼯⼚执⾏器选择器: 根据children获取选择器 */
chooser = chooserFactory.newChooser(children);
// 为每一个单例线程池添加一个关闭监听器
/** 3.为每个EventLoop线程添加线程终⽌监听器*/
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};
for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
/** 4. 将children 添加到对应的set集合中去重,表⽰只可读。*/
// 将所有的单例线程池添加到一个HashSet中
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
复制代码
总结⼀下上⾯的初始化步骤(3)中的⼀些重点:
- NIOEventLoopGroup的线程池实现其实就是⼀个NIOEventLoop数组,⼀个NIOEventLoop可以理解成就是⼀个线程。
- 所有的NIOEventLoop线程是使⽤相同的 executor、SelectorProvider、SelectStrategyFactory、RejectedExecutionHandler以及是属于某⼀个NIOEventLoopGroup的。 这⼀点从newChild(executor, args); ⽅法就可以看出:newChild()的实现是在NIOEventLoopGroup中实现的。
protected EventLoop newChild(Executor executor, Object... args) throws Exception { returnnew NioEventLoop(this, executor, (SelectorProvider) args[0], ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]); } 复制代码
- 当有IO事件来时,需要从线程池中选择⼀个线程出来执⾏,这时候的NioEventLoop选择策略是由GenericEventExecutorChooser实现的, 并调⽤该类的
next()
⽅法获取到下⼀个 NioEventLoop.
到了这⾥线程池的初始化就已经结束了, 基本这部分就只涉及 netty线程池的内容,不涉及到channel 与 channelPipeline和ChannelHandler等内容。下⾯的内容就是分析 NioEventLoop的构造器实现了。
3. NioEventLoop
Netty的NioEventLoop并不是一个纯粹的I/O线程,它除了负责I/O的读写之外,还兼顾处理以下两类任务:
系统Task
:通过调用NioEventLoop的execute(Runnable task)方法实现,Netty有很多系统Task,创建它们的主要原因是:当I/O线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由I/O线程负责执行,这样就实现了局部无锁化。定时任务
:通过调用NioEventLoop的schedule(Runnable command, long delay, TimeUnit unit)方法实现。
正是因为NioEventLoop具备多种职责,所以它的实现比较特殊,它并不是个简单的Runnable。
3.1 NioEventLoop类层次结构
它实现了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,正是因为这种设计,导致NioEventLoop和其父类功能实现非常复杂。
NioEventLoop 继承于 SingleThreadEventLoop; SingleThreadEventLoop ⼜继承于 SingleThreadEventExecutor; SingleThreadEventExecutor 是 netty 中对本地线程的抽象, 它内部有⼀个 Thread thread 属性, 存储了⼀个本地 Java 线程. 因此我们可以认为, ⼀个 NioEventLoop 其实和⼀个特定的线程绑定, 并且在其⽣命周期内, 绑定的线程都不会再改变。
NioEventLoop 的类层次结构图还是⽐较复杂的, 不过我们只需要关注⼏个重要的点即可. ⾸先 NioEventLoop 的继承链如下:
NioEventLoop -> SingleThreadEventLoop -> SingleThreadEventExecutor -> AbstractScheduledEventExecutor
在 AbstractScheduledEventExecutor 中, Netty 实现了 NioEventLoop 的 schedule 功能, 即我们可以通过调⽤⼀个 NioEventLoop 实例的 schedule()⽅法来运⾏⼀些定时任务. ⽽在 SingleThreadEventLoop 中, ⼜实现了任务队列的功能, 通过它, 我们可以调⽤⼀个NioEventLoop 实例的 execute() ⽅法来向任务队列中添加⼀个 task, 并由 NioEventLoop 进⾏调度执⾏.
3.2 NioEventLoop 的实例化过程
对于NioEventLoop的实例化,基本就是在NioEventLoopGroup.newChild() 中调⽤的,下⾯先给出源码:
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
SelectorProvider selectorProvider = (SelectorProvider) args[0];
SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
EventLoopTaskQueueFactory taskQueueFactory = null;
EventLoopTaskQueueFactory tailTaskQueueFactory = null;
int argsLength = args.length;
if (argsLength > 3) {
taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
}
if (argsLength > 4) {
tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
}
return new NioEventLoop(this, executor, selectorProvider,
selectStrategyFactory.newSelectStrategy(),
rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
}
复制代码
从上⾯函数⾥⾯ new NioEventLoop()出发分析实例化过程。
(1) 最先调⽤NioEventLoop ⾥⾯的构造函数:
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {
super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),
rejectedExecutionHandler);
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
复制代码
需要注意的是:构造器⾥⾯传⼊了 NioEventLoopGroup、Executor、SelectorProvider、SelectStrategyFactory、 RejectedExecutionHandler。从这⾥可以看出,⼀个NioEventLoop属于某⼀个NioEventLoopGroup, 且处于同⼀个NioEventLoopGroup下的所有NioEventLoop 公⽤Executor、SelectorProvider、SelectStrategyFactory和 RejectedExecutionHandler。
还有⼀点需要注意的是,这⾥的SelectorProvider构造参数传⼊的是通过在NioEventLoopGroup⾥⾯的构造器⾥⾯的 SelectorProvider.provider(); ⽅式获取的, ⽽这个⽅法返回的是⼀个单例的SelectorProvider, 所以所有的NioEventLoop公⽤同⼀个单例 SelectorProvider。
(2)核⼼的东西说完了,就是调⽤⽗类SingleThreadEventLoop的构造器:
protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue,
RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);
tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue");
}
复制代码
这⾥除了调⽤⽗类SingleThreadEventExecutor的构造器以外, 就是实例化了 tailTasks 这个变量;
对于tailTasks在SingleThreadEventLoop属性的定义如下:
privatefinal Queue<Runnable> tailTasks;// 尾部任务队列
复制代码
队列的数量maxPendingTasks 参数默认是SingleThreadEventLoop.DEFAULT_MAX_PENDING_TASK,其实就是 Integer.MAX_VALUE; 对于new的这个队列, 其实就是⼀个LinkedBlockingQueue ⽆界队列。
(3)再看调⽤的⽗类SingleThreadEventExecutor的构造器:
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, Queue<Runnable> taskQueue,
RejectedExecutionHandler rejectedHandler) {
super(parent); // 设置EventLoop所属于的EventLoopGroup
this.addTaskWakesUp = addTaskWakesUp;
this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
this.executor = ThreadExecutorMap.apply(executor, this);
this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue");
this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
复制代码
⾃此,NioEventLoop的实例化过程已经分析完毕。
前⾯已经分析完了EventLoopGroup和EventLoop,那么有⼀个问题,我们知道⼀个EventLoop实际上是对应于⼀个线程,那么这个EventLoop是什么时候启动的呢?
3.3 NioEventLoop 的启动顺序以及将EventLoop 与 Channel 的关联
在前⾯我们已经知道了, NioEventLoop 本⾝就是⼀个 SingleThreadEventExecutor, 因此 NioEventLoop 的启动, 其实就是 NioEventLoop 所绑定的本地 Java 线程的启动。 在NioEventLoop的构造器初始化分析过程中,我们知道,直到SingleThreadEventExecutor我们传⼊了⼀个线程执⾏器 Executor,我想线程的启动就是通过这个线程执⾏器启动的。
在SingleThreadEventExecutor类中,有⼀个⾮常重要的属性 thread
,这个线程也就是与NioEventLoop 所绑定的本地 Java 线程。我们看看这个线程是在什么时候初始化和启动的。
通过定位 thread 变量,发现在 doStartThread()
函数中,有⼀⾏代码:
thread = Thread.currentThread();
复制代码
除了这个地⽅,发现没有其余的地⽅对thread进⾏显⽰的实例化。⽽且这⾏代码在
executor.execute(new Runnable() {
}
复制代码
也就是说在executor执⾏的⼀个线程⾥⾯, 含义也就是当executor 第⼀次执⾏提交的任务创建的线程 赋值给 thread对象。由此可见,thread的启动其实也就是在这⾥。
通过不断的分析 doStartThread()
函数 的⽗调⽤关系,最顶层的调⽤就是 SingleThreadEventExecutor.execute(Runnable task)
也就是说哪⾥第⼀次调⽤了execute()函数,也就是启动了该NioEventLoop。下⾯就来分析⼀下最初是哪⾥调⽤了execute()函数,也就是NioEventLoop的启动流程。
在分析NioEventLoop的启动流程之前先来看看EventLoop 与 Channel 怎样关联的?
3.3.1 EventLoop 与 Channel 的关联
Netty 中, 每个 Channel 都有且仅有一个 EventLoop 与之关联
, 它们的关联过程如下:
从上图中我们可以看到, 当调用了 AbstractChannel#AbstractUnsafe.register 后, 就完成了 Channel 和 EventLoop 的关联. register 实现如下:
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 删除条件检查.
...
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new OneTimeTask() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
...
}
}
}
复制代码
在 AbstractChannel#AbstractUnsafe.register 中, 会将一个 EventLoop 赋值给 AbstractChannel 内部的 eventLoop 字段, 到这里就完成了 EventLoop 与 Channel 的关联过程.
3.3.2 EventLoop 的启动
根据之前的分析,我们现在的任务就是寻找 在哪里第一次调用了 SingleThreadEventExecutor.execute() 方法。
留心的读者可能已经注意到了, 我们在 EventLoop 与 Channel 的关联 这一小节时, 有提到到在注册 channel 的过程中, 会在 AbstractChannel#AbstractUnsafe.register 中调用 eventLoop.execute 方法, 在 EventLoop 中进行 Channel 注册代码的执行, AbstractChannel#AbstractUnsafe.register 部分代码如下:
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
//第一次调用eventLoop.execute()
eventLoop.execute(new OneTimeTask() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
...
}
}
复制代码
很显然, 一路从 Bootstrap.bind 方法跟踪到 AbstractChannel#AbstractUnsafe.register 方法, 整个代码都是在主线程中运行的, 因此上面的 eventLoop.inEventLoop() 就为 false, 于是进入到 else 分支, 在这个分支中调用了 eventLoop.execute. eventLoop 是一个 NioEventLoop 的实例, 而 NioEventLoop 没有实现 execute 方法, 因此调用的是 SingleThreadEventExecutor.execute()方法:
@Override
public void execute(Runnable task) {
...
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
复制代码
我们已经分析过了, inEventLoop == false, 因此执行到 else 分支, 在这里就调用了 startThread() 方法来启动 SingleThreadEventExecutor 内部关联的 Java 本地线程了。
总结一句话, 当 EventLoop.execute 第一次被调用时, 就会触发 startThread() 的调用, 进而导致了 EventLoop 所对应的 Java 线程的启动。
我们将 EventLoop 与 Channel 的关联 小节中的时序图补全后, 就得到了 EventLoop 启动过程的时序图:
3.4 NioEventLoop 启动之后是怎么实现事件循环的?
经过上面的分析我们已经找到了启动NIoEventLoop线程的入口,这里来分析一下NioEventLoop启动之后是如何实现所谓的事件循环机制的呢?
也就是从SingleThreadEventExecutor.execute(Runnable task);
从源码开始分析,对于一些不重要的源码我就直接删除了,只保留核心源码
public void execute(Runnable task) {
/**
* 判断Thread.currentThread()当前线程是不是与NioEventLoop绑定的本地线程;
* 如果Thread.currentThread()== this.thread, 那么只用将execute()方法中的task添加到任务队列中就好;
* 如果Thread.currentThread()== this.thread 返回false, 那就先调用startThread()方法启动本地线程,然后再将task添加到任务队列中.
*/
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();//启动 NioEventLoop
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
复制代码
上面的addTask(task);
函数实际上也就是将task 任务入taskQueue 队列中。然后就是看startThread();
是怎么启动 NioEventLoop.
private void startThread() {
if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
//设置thread 的状态 this.state是 ST_STARTED 已启动
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
//真正启动线程的函数, 其作用类似于 thread.start()
doStartThread();
}
}
}
复制代码
再看doStartThread() 是怎么实现的, 这个函数实现依旧比较复杂,我只取出核心的业务逻辑:
private void doStartThread() {
//启动线程之前,必须保证thread 是null,其实也就是thread还没有启动。
assert thread == null;
/** 通过executor启动一个新的task, 在task里面启动this.thread线程。*/
executor.execute(new Runnable() {
@Override
public void run() {
// 1. 将当前thread 赋值给 this.thread 也就是将启动的线程赋值给本地绑定线程thread
thread = Thread.currentThread();
// 2. 实际上是调用NioEventLoop.run() 方法实现 事件循环机制
try {
SingleThreadEventExecutor.this.run();//最核心所在,调用run()方法
success = true;
} catch (Throwable t) {
//........
} finally {
//确保事件循环结束之后,关闭线程,清理资源。
//...........
}
}//end run()
});// end run() 这个task
}// end doStartThread method
复制代码
3.4.1 thread的run() 函数
从上面可知,核心就是调用SingleThreadEventExecutor.this.run();
这是一个抽象函数,在NioEventLoop实现了这个函数,下面依然只给出能说明核心思想的核心源码,不重要的源码被我删除了。
protected void run() {
/** 死循环:NioEventLoop 事件循环的核心就是这里! */
for (;;) {
try {
// 1.通过 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
//2. 当有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);
}
//
}
} //end of run()函数
复制代码
上面函数中的一个死循环 for(;;) 就是NioEventLoop事件循环执行机制。死循环中的业务简单点就是:
- 通过调用 select/selectNow 函数,等待 IO 事件;
- 当有IO事件就绪时, 获取事件类型,分别处理这些IO事件,处理IO事件函数调用就是
processSelectedKeys();
下面对上面过程进行详解,分两步:IO事件轮询、IO事件的处理。
3.4.2 IO事件轮询
首先, 在 run() 方法中, 第一步是调用 hasTasks() 方法来判断当前任务队列中是否有任务:
protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty();
}
复制代码
这个方法很简单, 仅仅是检查了一下 taskQueue 是否为空. 至于 taskQueue 是什么呢, 其实它就是存放一系列的需要由此 EventLoop 所执行的任务列表. 关于 taskQueue, 我们这里暂时不表, 等到后面再来详细分析它.
1)当 taskQueue 不为空时, hasTasks() 就会返回TRUE,那么selectStrategy.calculateStrategy()
的实现里面就会执行selectSupplier.get()
而get()方法里面会调用 selectNow();
执行立即返回当前就绪的IO事件的个数,如果存在IO事件,那么在switch 语句中就会直接执行 default, 直接跳出switch语句,如果不存在,就是返回0, 对应于continue,忽略此次循环。
2)当taskQueue为空时,就会selectStrategy.calculateStrategy()
就会返回SelectStrategy.SELECT, 对用于switch case语句就是执行select()函数,阻塞等待IO事件就绪。
3.4.3 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. 那什么是 oRatio呢? 它表示的是此线程分配给 IO 操作所占的时间比(即运行 processSelectedKeys 耗时在整个循环中所占用的时间
). 例如 ioRatio 默认是 50, 则表示 IO 操作和执行 task 的所占用的线程执行时间比是 1 : 1. 当知道了 IO 操作耗时和它所占用的时间比, 那么执行 task 的时间就可以很方便的计算出来了。
当我们设置 ioRate = 70 时, 则表示 IO 运行耗时占比为70%, 即假设某次循环一共耗时为 100ms, 那么根据公式, 我们知道 processSelectedKeys() 方法调用所耗时大概为70ms(即 IO 耗时), 而 runAllTasks() 耗时大概为 30ms(即执行 task 耗时).
当 ioRatio 为 100 时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); 而当 ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时), 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 runAllTasks().
我们这里先分析一下 processSelectedKeys() 方法调用,源码如下:
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
复制代码
这个方法中, 会根据 selectedKeys 字段是否为空, 而分别调用 processSelectedKeysOptimized 或 processSelectedKeysPlain. selectedKeys 字段是在调用 openSelector() 方法时, 根据 JVM 平台的不同, 而有设置不同的值, 在我所调试这个值是不为 null 的. 其实 processSelectedKeysOptimized 方法 processSelectedKeysPlain 没有太大的区别, 为了简单起见, 我们以 processSelectedKeysOptimized 为例分析一下源码的工作流程吧.
private void processSelectedKeysOptimized() {
//1. 迭代 selectedKeys 获取就绪的 IO 事件, 然后为每个事件都调用 processSelectedKey 来处理它.
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
//2. 为事件调用processSelectedKey方法来处理 对应的事件
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
复制代码
其实你别看它代码挺多的, 但是关键的点就两个: 迭代 selectedKeys 获取就绪的 IO 事件, 然后为每个事件都调用 processSelectedKey 来处理它。
还有一点需要注意的是, 我们可以调用 selectionKey.attach(object) 给一个 selectionKey 设置一个附加的字段, 然后可以通过 Object attachedObj = selectionKey.attachment() 获取它. 上面代代码正是通过了 k.attachment() 来获取一个附加在 selectionKey 中的对象, 那么这个对象是什么呢? 它又是在哪里设置的呢? 我们再来回忆一下 SocketChannel 是如何注册到 Selector 中的:
在客户端的 Channel 注册过程中, 会有如下调用链:
Bootstrap.initAndRegister ->
AbstractBootstrap.initAndRegister ->
MultithreadEventLoopGroup.register ->
SingleThreadEventLoop.register ->
AbstractUnsafe.register ->
AbstractUnsafe.register0 ->
AbstractNioChannel.doRegister
复制代码
最后的 AbstractNioChannel.doRegister 方法会调用 SocketChannel.register 方法注册一个 SocketChannel 到指定的 Selector:
@Override
protected void doRegister() throws Exception {
// 省略错误处理
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
}
复制代码
特别注意一下 register 的第三个参数, 这个参数是设置 selectionKey 的附加对象的, 和调用 selectionKey.attach(object) 的效果一样. 而调用 register 所传递的第三个参数是 this, 它其实就是一个 NioSocketChannel 的实例. 那么这里就很清楚了, 我们在将 SocketChannel 注册到 Selector 中时, 将 SocketChannel 所对应的 NioSocketChannel 以附加字段的方式添加到了selectionKey 中.
再回到 processSelectedKeysOptimized 方法中, 当我们获取到附加的对象后, 我们就调用 processSelectedKey 来处理这个 IO 事件:
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
复制代码
processSelectedKey 方法源码如下:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
//......
//......
try {
int readyOps = k.readyOps();
// 连接事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
//可写事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
//可读事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
// 这里就是核心中的核心了. 事件循环器读到了字节后, 就会将数据传递到pipeline
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
复制代码
这个代码是不是很熟悉啊? 完全是 Java NIO 的 Selector 的那一套处理流程嘛!
processSelectedKey 中处理了三个事件, 分别是:
- OP_READ, 可读事件, 即 Channel 中收到了新数据可供上层读取.\
- OP_WRITE, 可写事件, 即上层可以向 Channel 写入数据.\
- OP_CONNECT, 连接建立事件, 即 TCP 连接已经建立, Channel 处于 active 状态.\
下面我们分别根据这三个事件来看一下 Netty 是怎么处理的吧.
OP_READ 处理
当就绪的 IO 事件是 OP_READ, 代码会调用 unsafe.read() 方法, 即:
// 可读事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
复制代码
unsafe 这个字段, 我们已经和它打了太多的交道了, 在分析Bootstrap 时我们已经对它进行过浓墨重彩地分析了, 最后我们确定了它是一个 NioSocketChannelUnsafe 实例, 负责的是 Channel 的底层 IO 操作.
我们可以利用 Intellij IDEA 提供的 Go To Implementations 功能, 寻找到这个方法的实现. 最后我们发现这个方法没有在 NioSocketChannelUnsafe 中实现, 而是在它的父类 AbstractNioByteChannel 实现的, 它的实现源码如下:
@Override
public final void read() {
...
ByteBuf byteBuf = null;
int messages = 0;
boolean close = false;
try {
int totalReadAmount = 0;
boolean readPendingReset = false;
do {
byteBuf = allocHandle.allocate(allocator);
int writable = byteBuf.writableBytes();
int localReadAmount = doReadBytes(byteBuf);
// 检查读取结果.
...
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
...
totalReadAmount += localReadAmount;
// 检查是否是配置了自动读取, 如果不是, 则立即退出循环.
...
} while (++ messages < maxMessagesPerRead);
pipeline.fireChannelReadComplete();
allocHandle.record(totalReadAmount);
if (close) {
closeOnRead(pipeline);
close = false;
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close);
} finally {
}
}
复制代码
read() 源码比较长, 我为了篇幅起见, 删除了部分代码, 只留下了主干. 不过我建议读者朋友们自己一定要看一下 read() 源码, 这对理解 Netty 的 EventLoop 十分有帮助.
上面 read 方法其实归纳起来, 可以认为做了如下工作:
- 分配 ByteBuf
- 从 SocketChannel 中读取数据;
- 调用 pipeline.fireChannelRead 发送一个inbound 事件.
前面两点没什么好说的, 第三点 pipeline.fireChannelRead 读者朋友们看到了有没有会心一笑地感觉呢? 反正我看到这里时是有的。 pipeline.fireChannelRead 正好就是 inbound 事件起点. 当调用了 pipeline.fireIN_EVT() 后, 那么就产生了一个 inbound 事件, 此事件会以 head -> customContext -> tail 的方向依次流经 ChannelPipeline 中的各个 handler.
调用了 pipeline.fireChannelRead 后, 就是 ChannelPipeline 中所需要做的工作了。
OP_WRITE 处理
OP_WRITE 可写事件代码如下. 这里代码比较简单, 没有详细分析的必要了.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
复制代码
OP_CONNECT 处理
最后一个事件是 OP_CONNECT, 即 TCP 连接已建立事件.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
复制代码
OP_CONNECT 事件的处理中, 只做了两件事情:
- 正如代码中的注释所言, 我们需要将 OP_CONNECT 从就绪事件集中清除, 不然会一直有 OP_CONNECT 事件.
- 调用 unsafe.finishConnect() 通知上层连接已建立 .
unsafe.finishConnect() 调用最后会调用到 pipeline().fireChannelActive()
, 产生一个 inbound 事件, 通知 pipeline 中的各个 handler TCP 通道已建立(即 ChannelInboundHandler.channelActive 方法会被调用)
到了这里, 我们整个 NioEventLoop 的 IO 操作部分已经了解完了, 接下来的一节我们要重点分析一下 Netty 的任务队列机制.
3.5 netty的任务队列机制
我们已经提到过, 在Netty 中, 一个 NioEventLoop 通常需要肩负起两种任务, 第一个是作为 IO 线程, 处理 IO 操作; 第二个就是作为任务线程, 处理 taskQueue 中的任务. 这一节的重点就是分析一下 NioEventLoop 的任务队列机制的.
3.5.1 任务的添加
3.5.1.1 普通 Runnable 任务
NioEventLoop 继承于 SingleThreadEventExecutor, 而 SingleThreadEventExecutor 中有一个 Queue taskQueue 字段, 用于存放添加的 Task. 在 Netty 中, 每个 Task 都使用一个实现了 Runnable 接口的实例来表示.
例如当我们需要将一个 Runnable 添加到 taskQueue 中时, 我们可以进行如下操作:
EventLoop eventLoop = channel.eventLoop();
eventLoop.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello, Netty!");
}
});
复制代码
当调用 execute 后, 实际上是调用到了 SingleThreadEventExecutor.execute() 方法, 它的实现如下:
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
复制代码
而添加任务的 addTask 方法的源码如下:
protected void addTask(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
if (isShutdown()) {
reject();
}
taskQueue.add(task);
}
复制代码
因此实际上, taskQueue 是存放着待执行的任务的队列。
3.5.1.2 schedule 任务
除了通过 execute 添加普通的 Runnable 任务外, 我们还可以通过调用 eventLoop.scheduleXXX 之类的方法来添加一个定时任务.
EventLoop 中实现任务队列的功能在超类 SingleThreadEventExecutor 实现的, 而 schedule 功能的实现是在 SingleThreadEventExecutor 的父类, 即 AbstractScheduledEventExecutor 中实现的.
在 AbstractScheduledEventExecutor 中, 有以 scheduledTaskQueue 字段:
Queue<ScheduledFutureTask<?>> scheduledTaskQueue;
复制代码
scheduledTaskQueue 是一个队列(Queue), 其中存放的元素是 ScheduledFutureTask. 而 ScheduledFutureTask 我们很容易猜到, 它是对 Schedule 任务的一个抽象.
我们来看一下 AbstractScheduledEventExecutor 所实现的 schedule 方法吧:
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
ObjectUtil.checkNotNull(command, "command");
ObjectUtil.checkNotNull(unit, "unit");
if (delay < 0) {
throw new IllegalArgumentException(
String.format("delay: %d (expected: >= 0)", delay));
}
return schedule(new ScheduledFutureTask<Void>(
this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}
复制代码
这是其中一个重载的 schedule, 当一个 Runnable 传递进来后, 会被封装为一个 ScheduledFutureTask 对象, 这个对象会记录下这个 Runnable 在何时运行、已何种频率运行等信息.
当构建了 ScheduledFutureTask 后, 会继续调用 另一个重载的 schedule 方法:
<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
if (inEventLoop()) {
scheduledTaskQueue().add(task);
} else {
execute(new OneTimeTask() {
@Override
public void run() {
scheduledTaskQueue().add(task);
}
});
}
return task;
}
复制代码
在这个方法中, ScheduledFutureTask 对象就会被添加到 scheduledTaskQueue 中了。
3.5.2 任务的执行
当一个任务被添加到 taskQueue 后, 它是怎么被 EventLoop 执行的呢?
让我们回到 NioEventLoop.run() 方法中, 在这个方法里, 会分别调用 processSelectedKeys() 和 runAllTasks() 方法, 来进行 IO 事件的处理和 task 的处理. processSelectedKeys() 方法我们已经分析过了, 下面我们来看一下 runAllTasks()
中到底有什么名堂吧。
runAllTasks 方法有两个重载的方法, 一个是无参数的, 另一个有一个参数的. 首先来看一下无参数的 runAllTasks:
protected boolean runAllTasks() {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
return false;
}
for (;;) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception.", t);
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
return true;
}
}
}
复制代码
我们前面已经提到过, EventLoop 可以通过调用 EventLoop.execute 来将一个 Runnable 提交到 taskQueue 中, 也可以通过调用 EventLoop.schedule 来提交一个 schedule 任务到 scheduledTaskQueue 中. 在此方法的一开始调用的
fetchFromScheduledTaskQueue() 其实就是将 scheduledTaskQueue 中已经可以执行的(即定时时间已到的 schedule 任务) 拿出来并添加到 taskQueue 中, 作为可执行的 task 等待被调度执行,源码如下:
private void fetchFromScheduledTaskQueue() {
if (hasScheduledTasks()) {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
for (;;) {
Runnable scheduledTask = pollScheduledTask(nanoTime);
if (scheduledTask == null) {
break;
}
taskQueue.add(scheduledTask);
}
}
}
复制代码
接下来 runAllTasks() 方法就会不断调用 task = pollTask()
从 taskQueue 中获取一个可执行的 task, 然后调用它的 run() 方法来运行此 task.
注意, 因为 EventLoop 既需要执行 IO 操作, 又需要执行 task, 因此我们在调用 EventLoop.execute 方法提交任务时, 不要提交耗时任务, 更不能提交一些会造成阻塞的任务, 不然会导致我们的 IO 线程得不到调度, 影响整个程序的并发量。
这里也是为什么用我们自己的线程池隔离一些可能阻塞的业务。
参考文档
Netty学习和源码分析github地址
Netty从入门到精通视频教程(B站)
Netty权威指南 第二版
转:EventLoopGroup与EventLoop 分析