NioEventLoop 既会处理I/O事件,也会处理普通任务与定时任务
结构组成
主要组件:
线程与队列:
EventLoop执行任务、处理I/O事件的线程存在于SingleThreadEventExecutor中,executor 与 thread 是同一个线程,且 executor 是只有一个线程的线程池,相较于 thread,executor 支持更多的线程方法
由于是单线程,同一时间只能执行一个任务,当有多个任务时会将任务先暂存在 taskQueue 中,后续让线程依序执行
定时任务会存放在 AbstractScheduledEventExecutor 中
分析:
NioEventLoop的创建:
![image.png](https://cdn.nlark.com/yuque/0/2022/png/21405095/1649771721211-3f4618dc-d703-4a3d-8a7c-1ef821014592.png#clientId=ua97b6261-5ae9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=313&id=uc0719e18&margin=%5Bobject%20Object%5D&name=image.png&originHeight=503&originWidth=1005&originalType=binary&ratio=1&rotation=0&showTitle=false&size=38860&status=done&style=stroke&taskId=u3890afe6-1b8e-4d0d-a01d-7fbe8f501d6&title=&width=626)<br />**流程示意图**
从 NioEventLoop 的构造方法一路追踪,最终可得默认线程数的创建,为当前CPU核数X2
默认线程数配置位置
继续追踪,最终来到 MultithreadEventExecutorGroup ,其构造方法将完成 NioEventLoop 的创建,
每次执行任务时都会创建一个线程实体,其线程实体为 Netty 内部自行封装的 FastThreadLocalThread
每个线程实体创建时会通过其参数 newDefaultThreadFactory 创建 NioEventLoop 线程名称,命名规则为
nioEventloop-上一个NioEventLoopGroup的序号-上一个Group对应的NioEventLoop序号
在 newChild() 中保存创建的线程执行器、创建一个MpscQueue保证业务队列、创建Selector进行轮训
chooserFactory.newChooser() 为新连接绑定对应的 NioEventLoop,具体流程为判断 NioEventLoop数量 是否为2的次幂,如果是则使用优化方法PowerOfTwoEventExecutorChooser,不是则使用 GenericEventExecutorChooser ,优化方法计算规则为 index++ &(length-1) ,普通方法使用 abs(index++ % lengrh) ,二进制&操作效率比取模更高,取模需要使用计算机底层库,较为低效
Selector创建过程:
NioEventLoop 在构造时会对 Selector 进行创建,会创建出两个 Selector,其中一个是将原本 Selector 的 Set<_SelectionKey> _转换为数组形式以达到性能提升的目的
创建方式与Nio的Selector创建方法一致
通过暴力反射转换Set
NIO线程启动:
对服务端bind方法进行追踪,可以得知当eventLoop首次调用execute方法时启动,且不会进行多次启动
NIO线程启动大致流程
execute 方法结束后会阻塞当前 selector ,以达到节约资源的效果,待下一个 execute 执行时将会唤醒阻塞的 selector
当没有任务时会进入 SelectStrategy.SELECT 分支,当有任务时calculateStrategy方法会执行NioEventLoop内部的selectNow方法并顺带拿到IO事件
首次执行 execute 方法后,后续其他线程执行 execute 方法将会执行 EventLoop 的 wakeup 方法来唤醒Selector,通过原子操作确保同一时间 wakeup 只会执行一次,避免性能浪费
Nio空轮询Bug修复:
Linux环境下可能极小概率会出现Nio的selector空轮询问题,Netty的解决方案为当超过预定阈值时重新创建selector
ioRatio的作用:
当ioRatio值设为100时将会先执行I/O事件然后再执行所有的普通任务,由于是单线程执行,当任务数量过多时会造成时间开销巨大,因此在参数配置时不能将该值设为100,该值默认为50
Selector优化:
在 NeioEventLoop 构造函数执行时会创建 selector, 默认情况下会通过反射将 selector 底层的 HashSet 转换为数组方式进行优化,在处理每个 KeySet 时都会拿到对应的 Attachment,Attachment就是在向 Selector 注册IO事件的时候绑定的经过Netty封装过后的Channel
通过反射拿到selectedKeys与publicSelectedKeys,对其底层HashSet进行优化
处理I/O事件:
进入 processSelectedKeys 方法,通过判断 selectedKeys 进行对应IO事件的处理,区分实现如下图所示
定时任务的创建与添加:
Netty 会将定时任务封装为 ScheduledFutureTask
进入 schedule(),会判断当前是否为 NioEventLoop 发起的 schedule,还是外部线程发起的 schedule。如果是外部线程发起的,为了保证线程安全,会把添加操作转换为线程安全的操作;如果是 NioEventLoop 发起的,就直接进行添加
会把添加操作转换为线程安全的操作原因是定时任务队列的实现是非线程安全的
默认为普通Queue,并非线程安全
runAllTasks方法:
NioEventLoop 执行逻辑的最后过程就是 runAllTasks() 【执行任务】,执行任务分为两种,一种是普通任务,一种是定时任务。Netty在执行任务时会将定时任务存放到普通任务队列中,然后挨个执行任务
在每次执行64个任务后都会计算当前时间是否超过最大执行时间,如果超过则进行中断,中断后进行下一次的 NioEcentLoop 循环
其内部 fetchFromScheduledTaskQueue() 负责任务聚合,其 pollScheduledTask() 从定时任务队列获取截止时间为当前的定时任务,如果获取的定时任务不为空,则尝试将定时任务放到普通任务队列中,如果添加失败,将定时任务重新添加到定时任务队列避免任务丢失
fetchFromScheduledTaskQueue() 源码分析
总结:
用户代码在创建 BossGroup 和 WorkGroup 时 NioEventLoop 被创建,默认不传参数的时候创建2倍CPU核数的 NioEventLoop,每个 NioEventLoop 都有一个Chooser进行线程分配,Chooser 也会针对 NioEventrLoop 的个数进行优化
NioEventrLoop 在创建时会创建一个 selector 和定时任务队列,在创建 selector 时,Netty 会通过反射的方式用数组实现来替换 selector 内部原有的 HashSet 数据结构
NioEventrLoop 在首次调用 execute 方法的时候启动线程,线程是 FastThreadLocalThread ,线程启动完毕后,Netty会将创建完毕的线程保存到成员变量,用于判断执行NioEventLoop里面的逻辑是否为本线程
NioEventrLoop 的执行逻辑在 run 方法内部,主要包括三个过程,第一个是检测IO事件,第二个是处理IO事件,最后是执行任务队列