Netty网络编程第五卷

x33g5p2x  于2022-02-07 转载在 其他  
字(4.0k)|赞(0)|评价(0)|浏览(366)

原本只打算写四卷,结果第四卷结尾只分析到了netty启动初始化部分的源码,特开第五卷将netty剩余重点部分源码分析完毕

NioEventLoop 剖析

学习NioEventLoop之前,先要搞清楚NioEventLoop的重要组成是:Selector,线程,任务队列; 以及NioEventLoop既会处理io事件,也会处理普通任务和定时任务

NioEventLoop 线程不仅要处理 IO 事件,还要处理 Task(包括普通任务和定时任务),

NioEventLoop的探究过程从下面几个问题展开

1.Selector何时创建

先来看NioEventLoop这个类

找到了selector,下面找一下thread对象,thread对象要去其父类里面找

下面是继承图

在NioEventLoop的爷爷类SingleThreadEventExecutor中定义了线程对象和普通任务队列

找其曾祖父类AbstractScheduledEventExecutor

到此我们可以看出NioEventLoop具备处理io,普通任务和定时任务的能力
下面追踪一下Selector的初始化过程,首先就需要看NioEventLoop的构造方法了

对比下面nio源码:

到此,第一个问题回答完毕,selector是在NioEventLoop的构造方法中进行创建的

2.eventLoop为何有两个selector成员变量

下面分析为什么不用原生的selector,反而整出两个selector?

  • selector都有一个selectionKeys集合,该集合实现是set,set遍历性能不高,因为底层实现是哈希表
  • netty对此做了优化,将原生的selectionKeys底层的set集合换成了基于数组的实现

替换的源码展示如下:

为什么还要保留原始的seletor,而不直接使用包装后的selector?

  • 包装后的selector只是用来遍历selectionKeys的
  • 原生的selector的一些功能无法由包装后的selector实现,一些功能还是需要基于原生selector实现
    答案:为了在遍历selectedKeys时提高性能

3. eventloop的nio线程在何时启动

提交任务代码 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状态位来控制线程只会启动一次

4. 提交普通任务会不会结束select阻塞

书接上回,当nio线程创建完毕启动后,会进入一个死循环

新创建出来的nio线程不仅处理io事件,其他任务来了也需要处理,因此nio线程不能无限阻塞下去,需要在有任务的时候醒过来,因此我们可以推断出,当有新任务来的时候,会唤醒阻塞的nio线程,当超时时间到的时候,也会唤醒阻塞的nio线程

下面验证提交普通任务,是否会唤醒阻塞的nio线程

显然这里excute方法调用的就是子类SingleThreadEventExecutor的实现,为什么不是AbstractEventExecutor呢?,因为AbstractEventExecutor是SingleThreadEventExecutor的父类实现

这里由于继承链比较多,很多方法都是调用父类的方法,有时候有会调用子类的实现,看起来比较迷糊

提交普通任务会唤醒在select除阻塞住的nio线程,让他来处理我们提交的普通或者定时任务,而select阻塞监听的是io事件
总结:首先先从eventloopgroup中获取一个eventloop对象(本质是一个单线程执行器,内部维护一个thread对象)----》eventloop提交任务--->先将任务加入队列---->创建线程(通过线程工厂创建一个线程,赋值给成员变量thread),并用标记位防止重复创建--->thread赋值完毕后,会去运行传入的任务,该任务就是一个死循环,负责不断寻找新的可执行任务

5.wakeup方法中代码如何理解

NioEventLoop:

这里说的nio线程就是每个单线程执行器里面对应的成员变量thread

如果是nio线程自己去提交任务,不会执行wakeup(),它内部有唤醒的机制

为什么需要防止wakeup被重复调用呢? —>因为wakeup方法是重量级操作,很消耗系统资源

6.每次循环的时候,什么时候会进入SelectStrategy.SELECT分支

下面我们看一下selectNow()方法

当没有任务的时候,才会进入SELECT分支,当有任务的时候,会调用selectNow方法,顺带拿到io事件

7.何时会select阻塞,会阻塞多久

此时select的沉睡时间会被设置为long的最大值,即无限时长阻塞。直到有io事件发生,或者有任务出现,被其他线程唤醒

主要是为了防止定时任务不能被及时执行,因此如果存在定时任务,那么select就不能阻塞,或者在定时任务执行之前结束阻塞

执行完之后,再次进入下一轮循环,继续寻找任务和io事件进行处理

从上面的分析可以看出,一个创建完毕的nio线程,会不断循环处理io事件,普通任务和定时任务,还是非常勤恳的

8.nio空轮询bug在哪里体现,如何解决

nio的空轮询bug体现在,即使没有io事件发生,select也不会阻塞住,导致cpu占用率飙升,白白浪费cpu时间,并且如果同时存在好多个nio线程空轮询,cpu就被压榨没了

netty是如何解决这个bug的呢?

很简单,通过一个循环计数解决

每循环一次,计数加一

既然通过计数来防止空轮询bug,那么如何避免不是空轮询,而是真正有事件发生的循环导致计数累加呢?

jdk底层nio的selector的空轮询bug是发生在linux上面

9.ioRatio控制什么,设置100有什么作用

首先,我们必须明白,一个nio线程在一个死循环里面不断轮询执行各种io,普通,定时任务,如果任务的执行占据了很长的时间,那么io事件就不能够被及时处理

因此,为了避免普通任务执行时间过长影响io任务,我们需要一个参数来控制普通任务的执行时间占比

  • ioRatio:控制处理io事件的事件占用比例,默认是百分之50,一半时间用来处理io事件,一半时间用来处理任务

10.selectedKeys优化是怎么回事

源码体现在下面

当然,这里也不一定是NioServerSocketChannel,也可能是普通的channel,要看初始化的时候,传入的channel类型,因此a instanceof AbstractChannel的意图所在

这里selectionKey的优化体现在数组下标获取selectionKey的过程,而不是通过查找哈希桶,还可能需要查看对应哈希桶上的链表才能得到对应的结果

Accept流程

首先先看一下原生的nio实现

accept源码跟进

我们进入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处理器中完成

这里调用链非常长,下面直接刨根问底:

Read流程

总结

到此,简单的流程就走完了,后续还会出一卷,对netty整体架构进行分析,把上面的源码流程全部串起来走一遍

相关文章