原本只打算写四卷,结果第四卷结尾只分析到了netty启动初始化部分的源码,特开第五卷将netty剩余重点部分源码分析完毕
学习NioEventLoop之前,先要搞清楚NioEventLoop的重要组成是:Selector,线程,任务队列; 以及NioEventLoop既会处理io事件,也会处理普通任务和定时任务
NioEventLoop 线程不仅要处理 IO 事件,还要处理 Task(包括普通任务和定时任务),
NioEventLoop的探究过程从下面几个问题展开
先来看NioEventLoop这个类
找到了selector,下面找一下thread对象,thread对象要去其父类里面找
下面是继承图
在NioEventLoop的爷爷类SingleThreadEventExecutor中定义了线程对象和普通任务队列
找其曾祖父类AbstractScheduledEventExecutor
到此我们可以看出NioEventLoop具备处理io,普通任务和定时任务的能力
下面追踪一下Selector的初始化过程,首先就需要看NioEventLoop的构造方法了
对比下面nio源码:
到此,第一个问题回答完毕,selector是在NioEventLoop的构造方法中进行创建的
下面分析为什么不用原生的selector,反而整出两个selector?
替换的源码展示如下:
为什么还要保留原始的seletor,而不直接使用包装后的selector?
答案:为了在遍历selectedKeys时提高性能
提交任务代码 io.netty.util.concurrent.SingleThreadEventExecutor#execute
下面探究execute方法源码
首先我们主要execute方法所属的类---->SingleThreadEventExecutor从名字可以看出,这是一个单线程的任务执行器,这也很好理解,可以把NioEventLoopGroup看做一个线程池,线程池是懒加载的,并且线程池里面每一个线程执行器就是SingleThreadEventExecutor
还需要注意一点,我们上面在NioEventLoop的组成时分析的:,即SingleThreadEventExecutor有一个成员变量thread用来保存当前开启的线程,因为是懒加载的,初始为null
同理真正干活的是doStartThread方法
首先验证excutor中任务的调用时通过nio线程异步调用的
下面我们再继续验证
线程工厂创建出来的线程启动执行run方法,run方法中会将新创建的线程赋值给单线程执行器的thread对象,
并且该线程执行完run方法后不会销毁,会进入死循环不断寻找新的任务执行
答案:当首次调用execute方法的时,nio线程启动,并且通过一个state状态位来控制线程只会启动一次
书接上回,当nio线程创建完毕启动后,会进入一个死循环
新创建出来的nio线程不仅处理io事件,其他任务来了也需要处理,因此nio线程不能无限阻塞下去,需要在有任务的时候醒过来,因此我们可以推断出,当有新任务来的时候,会唤醒阻塞的nio线程,当超时时间到的时候,也会唤醒阻塞的nio线程
下面验证提交普通任务,是否会唤醒阻塞的nio线程
显然这里excute方法调用的就是子类SingleThreadEventExecutor的实现,为什么不是AbstractEventExecutor呢?,因为AbstractEventExecutor是SingleThreadEventExecutor的父类实现
这里由于继承链比较多,很多方法都是调用父类的方法,有时候有会调用子类的实现,看起来比较迷糊
提交普通任务会唤醒在select除阻塞住的nio线程,让他来处理我们提交的普通或者定时任务,而select阻塞监听的是io事件
总结:首先先从eventloopgroup中获取一个eventloop对象(本质是一个单线程执行器,内部维护一个thread对象)----》eventloop提交任务--->先将任务加入队列---->创建线程(通过线程工厂创建一个线程,赋值给成员变量thread),并用标记位防止重复创建--->thread赋值完毕后,会去运行传入的任务,该任务就是一个死循环,负责不断寻找新的可执行任务
NioEventLoop:
这里说的nio线程就是每个单线程执行器里面对应的成员变量thread
如果是nio线程自己去提交任务,不会执行wakeup(),它内部有唤醒的机制
为什么需要防止wakeup被重复调用呢? —>因为wakeup方法是重量级操作,很消耗系统资源
下面我们看一下selectNow()方法
当没有任务的时候,才会进入SELECT分支,当有任务的时候,会调用selectNow方法,顺带拿到io事件
此时select的沉睡时间会被设置为long的最大值,即无限时长阻塞。直到有io事件发生,或者有任务出现,被其他线程唤醒
主要是为了防止定时任务不能被及时执行,因此如果存在定时任务,那么select就不能阻塞,或者在定时任务执行之前结束阻塞
执行完之后,再次进入下一轮循环,继续寻找任务和io事件进行处理
从上面的分析可以看出,一个创建完毕的nio线程,会不断循环处理io事件,普通任务和定时任务,还是非常勤恳的
nio的空轮询bug体现在,即使没有io事件发生,select也不会阻塞住,导致cpu占用率飙升,白白浪费cpu时间,并且如果同时存在好多个nio线程空轮询,cpu就被压榨没了
netty是如何解决这个bug的呢?
很简单,通过一个循环计数解决
每循环一次,计数加一
既然通过计数来防止空轮询bug,那么如何避免不是空轮询,而是真正有事件发生的循环导致计数累加呢?
jdk底层nio的selector的空轮询bug是发生在linux上面
首先,我们必须明白,一个nio线程在一个死循环里面不断轮询执行各种io,普通,定时任务,如果任务的执行占据了很长的时间,那么io事件就不能够被及时处理
因此,为了避免普通任务执行时间过长影响io任务,我们需要一个参数来控制普通任务的执行时间占比
源码体现在下面
当然,这里也不一定是NioServerSocketChannel,也可能是普通的channel,要看初始化的时候,传入的channel类型,因此a instanceof AbstractChannel的意图所在
这里selectionKey的优化体现在数组下标获取selectionKey的过程,而不是通过查找哈希桶,还可能需要查看对应哈希桶上的链表才能得到对应的结果
首先先看一下原生的nio实现
我们进入unsafe.read()方法体内部,注意如果是不同的事件,read方法的实现是不同的
该方法主要是创建SocketChannel,并设置到一个新的NioSocketChannel中,然后设置SocketChannel为非阻塞,拿到监听事件的ops.方便在后续注册到选择器上面,然后还会对nioSocketChannel进行初始化,主要是设置一些默认属性,例如默认pipeline
如果记得初始化过程的小伙伴,应该知道,这里的默认pipeline会创建出头尾两个处理器,然后初始化过程当通道就绪后,会触发active事件,调用每个处理器的active事件,在头处理器中,会处理read事件,如果是accept事件,会注册当前socketChannel到一个选择器上面
下面回到unsafe.read方法体内:
下面就是将socketChannel注册到选择器上面,然后关注读事件了,该源码体现部分如下
现在我们来回忆一下初始化过程中,在new channel()后的init()方法中,会添加一个初始化器,该初始化器在后续回调过程中,会向添加一个acceptor处理器,专门处理客户端连接事件的,这里触发的read方法重点就在于通过那个acceptor处理,完成通道在选择器上面的注册和监听事件
这个register和启动流程的register很相似
inEventloop作用就是判断当前调用方线程,和自身绑定的线程是否一致
上面excute方法执行时会新创建一个线程,来执行传入的任务,此时就完成了线程的切换,并且还会将新创建的thread绑定到当前channel上
其实就是在触发通道就绪事件这里完成了读事件的注册监听操作—》head处理器中完成
这里调用链非常长,下面直接刨根问底:
到此,简单的流程就走完了,后续还会出一卷,对netty整体架构进行分析,把上面的源码流程全部串起来走一遍