Loading

Netty源码学习2——NioEventLoop的执行

系列文章目录和关于我

零丶引入

在《Netty源码学习1——NioEventLoopGroup的初始化》中,我们学习了NioEventLoopGroup和NioEventLoop的初始化,在下面netty服务端启动的demo中

image-20230818150757349

会在ServerBootStrap中指定Channel为Nio类型的Channel,然后启动的时候绑定端口,之前我们解释道NioEventLoop是事件循环,这里的事件是指客户端连接,数据准备就绪等IO事件,循环是指会有一个线程不断的处理任务。so 这篇我们一起看下NioEventLoop是如何产生IO事件并执行各种任务的。

一丶NioEventLoop的初始化

这部分在[Netty源码学习1——NioEventLoopGroup的初始化#第四节](Netty源码学习1——NioEventLoopGroup的初始化 - Cuzzz - 博客园 (cnblogs.com))中进行了说明,其中比较有趣的是Netty对JDK原生Selector的优化,将原本基于HashSet的SelectionKey集合,修改为基于数组的SelectionKey集合,优化了迭代和修改的效率,再次就不在过多赘述了。

二丶NioEventLoop的启动

NioEventLoop中存在一个run方法,该方法是一个死循环,这便是事件循环中的循环,它由一个线程专门去执行,我们先分析下NioEventLoop#run方法是什么时候开始执行的。

image-20230818160755962

如上分别是客户端和服务端启动部分的源码,在bind 和 connect中都会触发——Channel的初始化+Channel的注册。

  1. Channel的初始化:由对应的Factory进行创建(默认使用反射)

  2. Channel的注册:这里的注册将调用NioEventLoopGroup#register方法,

    image-20230818161332251

    这里自然是通过EventExecutorChooser#next方法选择一个NioEventLoop进行注册

    image-20230818161625569

    image-20230818161846194

    在excute方法中会将EventLoop#run方法包装成一个Runnable提交到线程池中,这里的线程池是被包装后的ThreadPerTaskExecutor,每一个任务的提交都会新建一个线程去执行

    image-20230818162111791

    因此可以任务NioEventLoop#run方法由一个线程单独去执行

三丶NioEventLoop#run

1.SelectStrategy控制循环

和Tomcat的Poller类似,NioEventLoop需要使用Selector去进行IO多路复用,Netty抽象出SelectStrategy使用其calculateStrategy来控制循环

image-20230818164523102

image-20230818174638767

其中selectNow即使用JDK Selector#selectNow方法进行非阻塞select

image-20230818174845727

DefaultSelectStrategy#calculateStrategy的逻辑是,如果当前EventLoop中存在任务那么直接返回就绪的就绪SelectionKey数目,如果不存哎任务,那么返回常量SELECT(-1),根据返回的数字会进入不同的分支。

image-20230818181617099

2.执行阻塞select

image-20230818183604408

NioEventLoop支持调度任务,因此内部使用了优先队列保存调度任务,并且调度任务具备一个deadlineNanos属性

image-20230818184201875

image-20230818184706084

3.执行任务

NioEventLoop支持ioRatio指定IO事件的处理事件,和其他任务执行时间的比例

image-20230818185819749

其中处理IO时间的部分涉及到Netty的ChannelPipeline的执行,这部分在后续的博客中进行讲解

3.1 runAllTasks

无参的runAllTask会在IO事件处理结束后执行,首先会将scheduledTaskQueue中的任务(未到执行时间的任务,or deadlineNanos早于当前时间的任务 不会转移到taskQueue)转移到taskQueue,然后执行taskQueue中的任务,执行完taskQueue的任务后,执行tailTasks中的任务

image-20230818191432339

3.2 runAllTasks(超时时间)

大流程和上面runAllTasks类似,依旧是将调度任务从scheduledTaskQueue转移到taskQueue,然后执行taskQueue任务,然后执行tailTasks中的任务

image-20230818192120835

感觉比较有趣的是,对调度任务的处理,移动到taskQueue后并不会再次判断其deadlineNanos和当前时间的关系,不会再关注其是否过期。

3.3 处理NIO空轮询的bug

Java NIO在linux下基于epoll,但是因为poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为EPOLLHUP或者EPOLLERR,eventSet事件集合发生了变化,这会导致Selector会被唤醒,唤醒之后却没有感兴趣的事件,导致IO事件处理的循环不会执行,陷入到循环中.

image-20230818193354861

下面我们看看netty是如何处理此bug的

image-20230818193619887

image-20230818193934921

可以看到Netty通过重新构建Selector的方式去规避空轮询的bug,其中rebuildSelector最终调用rebuildSelector0,会将原本selector中的SelectionKey拷贝到新的selector,然后关闭旧的Selector

四丶总结

此节我们学习了NioEventLoop的执行原理,本质上还是通过Selector进行IO多路复用,理解了什么是事件循环,其中的事件指通过IO多路复用监听多路IO事件,循环是指循环的监听IO事件,并处理IO事件和其他任务,但是对于IO事件的处理我们并未做更多的学习,这部分将在下一篇中进行学习。

posted @ 2023-08-18 19:49  Cuzzz  阅读(450)  评论(0编辑  收藏  举报