Netty源码学习1——NioEventLoopGroup的初始化
零丶引入
netty源码学习中,大家maybe都接触到如下的hello world——netty客户端启动的demo:
映入眼帘的第一个类就是NioEventLoopGroup,很多文章上来就是是Netty中的核心类,啥Channel,Pipeline,Context,Boostrap一通劈里啪啦,我看起来比较费劲。
so本文不会上来就给大家介绍netty中所有的组件,而是先从NioEventLoopGroup入手。
一丶何为NioEventLoopGroup & NioEventLoopGroup继承关系
上图为NioEventLoopGroup继承关系,根据源码上的注释我们可以大概了解这些类的作用:
-
EventExecutorGroup:通过next方法提供EventExecutor,并且还负责处理它们的生命周期,并允许以全局方式关闭它们(指shutdownGracefully关闭EventExecutorGroup中的所有EventExecutor)
-
EventExecutor:EventExecutor 是一个特殊的 EventExecutorGroup(next方法指只会返回自己),它附带了一些方便的方法来查看线程是否在事件循环中执行。
-
EventLoopGroup:特殊EventExecutorGroup,允许注册在事件循环期间注册Channel
Channel是一个连接网络输入和IO处理的桥梁。可以通过Channel来判断当前的状态,是open还是connected,还可以判断当前Channel支持的IO操作。
-
AbstractEventExecutorGroup:EventExecutorGroup的抽象实现,可以看到对AbstractEventExecutorGroup提交任务,最终都会被其使用next获取EventExecutor进行处理。
EventExecutor是打工仔,AbstractEventExecutorGroup是分配任务的leader
-
MultithreadEventExecutorGroup:从名字上可以看出这是一个多线程的EventExecutorGroup,其中的“线程” = EventExecutor
-
MultithreadEventLoopGroup:多线程的EventExecutorGroup + EventLoopGroup = 多线程处理任务且允许Channel注册的EventLoopGroup,下面猫一眼其Channel的注册:
可以看到还是交给了打工人EventLoop
-
NioEventLoopGroup:MultithreadEventLoopGroup实现,其关联的Channel是基于NIO Selector的Channel实现的
二丶NioEventLoopGroup的初始化
在Netty 入门Demo中无论是Server还是Client都会先初始化NioEventLoopGroup,下面对于这个初始化过程进行源码解析。
-
参数中的SelectorProvider 是由SelectorProvider.provider() 提供的,SelectorProvider #openSelector可以创建selector,不同的操作系统这里会拿到不同的SelectorProvider
-
DefaultSelectStrategyFactory.INSTANCE是SelectStrategyFactory的默认实现,其newSelectStrategy会提供DefaultSelectStrategy作为选择策略,这个策略在Netty NioEventLoop#run方法来左右程序的执行(这点后续详细分析)
最终NioEventLoopGroup的构造方法将调用父类MultithreadEventExecutorGroup的构造方法
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
checkPositive(nThreads, "nThreads");
if (executor == null) {
// 初始化executor
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
// 创建EventLoop
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
// s省略关闭children数组中EventLoop的代码
}
}
// 选择器,EventLoopGroup#next方法依赖此选择器选择EventLoop
chooser = chooserFactory.newChooser(children);
// 此处省略 EventLoopGroup关闭的Future回调
// 只读set
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
其中比较有意思的是newChild方法,此方法由NioEventLoopGroup进行实现
且NioEventLoopGroup中Executor将作为参数进行使用,目前我尚不知这个Executor的作用,但是可以先看一下ThreadPerTaskExecutor特性
DefaultThreadFactory#newThread如下
可以看到每一个Thread都是FastThreadLocalThread,每一个任务都会包装为FastThreadLocalRunnable#wrap方法包装,以保证FastThreadLocal会在任务执行后进行释放(和TransmittableThreadLocal的做法类似,都是对原有Thread,和任务的包装)
Netty的FastThreadLocal的奥妙后续会单独进行学习和分析。
三丶NioEventLoop
在学习NioEventLoopGroup是如何创建NioEventLoop之前,我们先看下NioEventLoop的继承体现。
- SingleThreadEventExecutor:单线程Exectuor,内部持有一个Thread,在第一次提交任务的时候会将任务放到任务队列,并启动内部的Thread,该Thread执行逻辑交由子类实现
- EventLoop:Channel注册到EventLoop中,后续由EventLoop处理这些Channel
- SingleThreadEventLoop:SingleThreadEventExecutor + EventLoop = 单线程处理任务的EventLoop
四丶NioEventLoopGroup#newChild创建NioEventLoop
值得一说是还是这个executor参数,在NioEventLoop调用父类构造函数的时候,这个executor 会使用ThreadExecutorMap.apply包装一下,会将当前的NioEventLoop记录到FastThreadLocal中,让任务的执行过程可以从FastThreadLocal中拿到当前NioEventLoop
1.Netty对于Selector的优化
在NioEventLoop的创建时,会执行openSelector返回一对Selector,其中一个时原生的JDK中的Selector,另外一个时Netty优化的Selector,我们看下Netty做了什么优化
可以看到Netty会反射修该原JDK中的Selector 的selectedKeys和publicSelectedKeys字段,在原生JDK Selector中这两个Set的作用:
-
selectedKeys:
Selector
会将自己监听到的IO就绪
的Channel
放到selectedKeys
中 -
publicSelectedKeys:
selectedKeys
的视图,用于向外部线程返回IO就绪
的SelectionKey
。这个集合在外部线程中只能做删除操作不可增加元素
,并且`不是线程安全的
Netty为啥要进行优化昵
-
在
SelectorImpl
监听到IO就绪
的SelectionKey
后,会将就绪IO对应的`SelectionKey add到selectedKeys集合中 -
IO就绪后会唤醒阻塞在SelectorImpl#select方法上的线程,这些线程将调用SelectorImpl#selectedKeys,进行遍历处理,这里将返回publicSelectedKeys
这里的add和遍历操作都是针对set(HashSet)的,HashSet底层基于HashMap存在Hash冲突导致其插入需要使用拉链法,遍历的时候也无法使用 CPU 缓存的优势来提高遍历的效率(指数组元素都在紧密排布大概率在一个缓存行,可以利用预读优势)因此Netty将其优化为自己实现的SelectedSelectionKeySet
最终会将JDK元素的Selector包装为SelectedSelectionKeySetSelector
可以看到每次执行select方法会对selectedKeys进行处理,其实就是清除原生JDK Selector中的selectedKeys字段和publicSelectedKeys中的SelectionKey,为什么要这么做昵,让我们回顾原生JDK Selector的使用
在selector#selectedKeys读取到publicSelectedKeys中就绪的就绪的IO实现后,会进行处理,处理完成后会进行clear方法清除已经处理的SelectionKey,但是Netty优化后的SelectedSelectionKeySet是不支持remove的,而是在SelectedSelectionKeySetSelector下一次select的时候进行批量清除。
2.NioEventLoop任务队列的创建Mpsc队列
最终创建的MpscUnboundedArrayQueue或者MpscUnboundedAtomicArrayQueue,Mpsc是Multiple producers and single consumers
多生产者单消费者的缩写,它支持多个生产者多个消费者的情况并且性能强劲(cas无锁设计减少了频繁的唤醒和阻塞,内部使用数组在需要扩容的时候不是复制旧数组,而是数组尾部元素指向下一个数组)
3.NioEventLoop中的任务队列
事件循环中存在的事件队列,在NioEventLoop中的体现就是三个任务队列
- taskQueue:就是上面提到的Mpsc队列,NioEventLoop中的线程run会处理其中的任务
- tailTaskQueue:尾部队列,处理完taskQueue中所有任务后,会获取tailTaskQueue中的任务进行执行
- scheduledTaskQueue:PriorityQueue<ScheduledFutureTask<?>>默认是基于数组的堆,用来存在延时任务
五丶总结
学习了NioEventLoopGroup 和 NioEventLoop的初始化,后续NioEventLoop 事件循环中的循环进行源码分析。