Netty篇:Reactor线程模型和NioEventLoop,NioEventLoopGroup源码分析


Reactor线程模型


      Reactor模式是一种典型的事件驱动的编程模型,主要有单线程模型,多线程模型以及主从线程模型。


Reactor单线程模型

      Reactor单线程模型,指所有的I/O操作都在同一个NIO线程上面完成。包括建立TCP链接、编解码、接受和发送器请求应答消息等,模型如下图(图出自Scalable IO in Java)。

在这里插入图片描述

      通过Acceptor类接受客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行处理。



Reactor多线程模型


      单线程模型在高负载、大并发的应用场景中,会有性能瓶颈,CPU跑满也无法满足海量请求,随后导致连接超时,消息积压等问题,如果NIO线程跑飞或者死循环,会导致整个系统模块不可用。为了解决这些问题,演进除了Reactor多线程模型。


在这里插入图片描述

      一个专门的NIO线程Acceptor线程用于监听服务端,接收客户端的TCP链接请求。网络I/O操作,有一个NIO线程池负责,包含一个任务队列和N个可用的线程,由这些线程负责消息的读取、解码、编码和发送。



Reactor主从线程模型


      在绝大多数场景下,Reactor多线程模型可以满足性能需求。但在个别场景中,一个NIO线程负责监听和处理所有客户端连接可能会存在性能问题。例如并发百万客户端连接或者服务端要对客户端握手进行比较耗时的安全认证,单独一个Acceptor线程可能会存在性能不足的问题,于是出现了第三种Reactor线程模型——主从线程模型。

      主从Reactor线程模型在服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的I/O线程上,有I/O线程负责后续的I/O操作。


在这里插入图片描述



Netty的线程模型


      Netty根据配置可以支持Reactor单线程模型、多线程模型和主从多线程模型。

在这里插入图片描述


      服务器启动的时候,创建了连个NioEventLoop,他们实际是两个独立的Reactor线程池,一个用于接收客户端的TCP链接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时Task等。

      Netty接受客户端的请求的线程池的职责是:接收客户端连接,初始化Channel参数;将链路状态变更事件通知给ChannelPipeline。Netty处理I/O操作的Reactor线程池的职责是:异步读取通信对端的数据报,发送读事件到ChannelPipeline;异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;执行系统调用Task;执行定时任务Task。



NioEventLoopGroup


      在各种Netty开发的demo中,首先要做的就是声明两个NioEventLoopGroup,一个名为bossGroup,一个名为workGroup,分别对应主从Reactor线程模型中的mainReactor线程池和subReactor线程池。


在这里插入图片描述

      NioEventLoopGroup的集成关系如下图:

在这里插入图片描述

      NioEventLoopGroup初始化过程中依次调用其父类构造完成初始化操作,在父类MultithreadEventLoopGroup的静态块初始化时会设置其线程数,如果有配置择取配置数,如果没有则设置为CPU核心数*2的两倍。

      主要的初始化工作在MultithreadEventExecutorGroup中:


在这里插入图片描述

      如果executor为null,则创建一个eventLoop执行器ThreadPerTaskExecutor,eventloop的任务都是通过调用其的execute()函数,创建一个线程,执行提交的任务。然后循环调用newChild()方法创建NioEventLoop对象,构造方法中调用openSelector()(内部实现为provider.openSelector())启动到Selector,维护SelectorProvider,SelectorProvider,selector,selectStrategy等成员变量,创建两个MpscUnboundedArrayQueue队列tailTasks和taskQueue,实现代码如下:


在这里插入图片描述

      回到MultithreadEventExecutorGroup中,然后创建一个EventExecutorChooser对象,即事件执行选择器用于选择NioEventLoop在处理新的channel,有PowerOfTwoEventExecutorChooser和GenericEventExecutorChooser两种实现方式,前者使用‘&’位与操作获取EventExecutor索引值,后者使用‘%’取模运算。



NioEventLoop


       NioEventLoop对应Reactor的每条线程,事实上,每一个NioEventLoop也确实始终与一条线程绑定,主要工作为,根据select策略进行相应的select操作,处理准备好的I/O事件,执行系统Task和定时任务。继承关系如下图:

在这里插入图片描述

       NioEventLoop启动在bootstrap调用bind方法时,入口在AbstractBootStrap#doBind方法,bind方法代码如下:
在这里插入图片描述

       略过Channel等组件的初始化与注册流程,只关注NioEventLoop的启动流程,在完成Channel注册的时候会启动线程,入口在AbstractUnsafe#register方法,下篇文章会详细介绍:


在这里插入图片描述

      获取与channel绑定的NioEventLoop,执行其execute方法,即NioEventLoop线程的入口,将Runnable注册任务作为参数传入,实现在其父类SingleThreadEventExecutor中,具体实现代码如下图:

在这里插入图片描述

      判断当前线程是不是NioEventloop唯一绑定的线程,如果是就把任务放到任务队列中,如果不是就启动一个新的线程。进入启动线程方法startThread,代码如下:


在这里插入图片描述

      通过CAS改变线程状态然后执行doStartThread方法执行具体逻辑,关闭部分异常处理代码如下:

在这里插入图片描述

      此处的executor为ThreadPerTaskExecutor对象,执行其execute方法创建并绑定,启动,execute方法代码如下:


在这里插入图片描述

      启动线程后回到doStartThread方法执行SingleThreadEventExecutor.this.run();而这是SingleThreadEventExecutor的一个抽象方法,实际上会调用NioEventLoop类的run()方法,即真正的事件循环。



      NioEventLoop的核心逻辑都在重写的run方法中。该方法通过Java NIO Selector的多路复用来实现对多个Channel的监控,该方法还对epoll空轮询bug进行了解决,NioEventLoop的时间循环,处理I/O事件,系统Task,定时任务等都在run方法中处理,代码如下:

在这里插入图片描述

      swtich条件selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())中会判断如果没有待处理的任务,则会调用selectNowSupplier.get()方法,最终调用的是selector.selectNow()方法,返回就绪I/O事件个数,如果没有待处理的任务则返回SelectStrategy.SELECT,执行 select(wakenUp.getAndSet(false))方法,阻塞等待IO事件,同时著名的JDK NIO类库epoll空轮询bug也在这里边解决,代码如下:

在这里插入图片描述

      select方法有一个死循环,当有定时任务到达执行时间,或者收到了待处理的事件,或者有了新的Task需要处理是就会退出select方法去执行任务,select中维护了selectCnt来维护轮询次数,当selectCnt大于设定的SELECTOR_AUTO_REBUILD_THRESHOLD的值(默认512次),就会重置Selector,即将原Selector注册信息注册到新的Selector,用新的替换旧的。

      有了待处理的任务后回到run方法中,重置cancelledKeys和needsToSelectAgain,ioRatio是在事件循环中期待用于I/O操作时间的百分比,默认50%。如果不为100%,执行task时会传入可用于运行任务的时间。

      执行processSelectedKeys()方法,当存在感兴趣事件是进入processSelectedKeysOptimized()方法中,代码如下:


在这里插入图片描述

      因为启动流程中将NioServerSocketChannel设置到了SelectionKey中,所以这里k.attachement()获取的对象时NioServerSocketChannel对象,即AbstractNioChannel的实现类,进入processSelectedKey(k, (AbstractNioChannel) a)方法中:

在这里插入图片描述

      如果SelectionKey无效,且还被注册在当前EventLoop上,则关闭它。如果有效,则根据OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE等不同事件调用AbstractNioChannel内部接口NioUnsafe的不同实现类去执行(下文会详细介绍)。

      处理完I/O事件后回到run方法中,处理任务队列中的各种任务:


在这里插入图片描述

      主要完成了聚合任务,把到期的定时任务转移到普通任务队列,循环从普通任务队列中获取任务,调用run方法执行任务,每执行64个任务,判断是否到期。

      至此,NioEventLoop初始化工作流程简单介绍完毕。



参考:


《Netty权威指南》

你可能感兴趣的:(Netty篇:Reactor线程模型和NioEventLoop,NioEventLoopGroup源码分析)