Reactor的单线程和多线程
关于Reactor有一些地方想做下说明,因为看到网上说到这块内容的文章不是特别多,而提到这块的文章的一些用词可能会对一些同对Netty不是特别了解,或者正在学习Netty源码的同学造成一定困扰和误解,所以想在这里分享出来。这里有些理解可能和一些同学平时的认知不太相同。
如果本文所写有任何问题或者错误,欢迎指出讨论。
1.关于线程数
NioEventLoopGroup有多个构造方法,也有无参构造方法。
无参构造方法将nThreads设置为0。
0就根据CPU核数决定入参nThreads。
如果不是0,就用你传入的nThreads来决定入参。这个入参nThreads决定的是MultithreadEventLoopGroup中的EventExecutor数组的大小,也就是NioEventLoop的个数。
注意:上述描述针对的是MultithreadEventLoopGroup初始化时构造NioEventLoop的个数,而不是真正被使用的NioEventLoop数量
1)Boss线程组的NioEventLoop
对于Boss线程,内部线程是在bind端口的时候创建的,无论你在构造EventLoopGroup的时候的入参传入的nThreads是多少,真正创建并启动的Boss线程的个数只和bind的端口数有关。如果server只绑定了一个端口(大部分服务器都是只绑定一个端口),那么及时nThreads>1,那么也只会启动一个Boss线程。
原因就是,这个线程的启动是bind动作的附属操作(bind中的register0),所以和bind的次数相关。
2)Worker线程组的NioEventLoop
对于Worker线程,内部线程是在其他动作,比如客户端发起连接/读等操作的时候,由Boss的NioEventLoop接收,并放入任务队列。
在channelRead的时候发起register操作,并启动Worker线程组的线程,后面由Worker组的NioEventLoop来处理这些task,所以其个数取决于多少个不同客户端的连接触发这个channelRead,因为对该Worker线程组的chooser是取余的,所以当启动的线程数到nThreads,就会根据算法选择nThreads内的某个线程,所以这个创建的线程数的上限就是nThreads。
3)Boss和Worker的线程分工
Boss和Worker对应的NioEventLoop里面都open了Selector(在init方法中的openSelector方法),但是它们的职责是不同的。
最开始用户线程中创建NioEventLoop时会openSelector开启一个Selector,然后创建ServerSocketChannel,并把该channel注册到Selector上,然后再执行channel的bind。这个过程会启动该NioEventLoop的线程,即Boss线程。
在Boss线程中,Boss的Selector轮询创建连接事件(OP_ACCEPT),然后创建socketChannel(注意不是ServerSocketChannel),并对其初始化,然后从Worker组中选择一个NioEventLoop。
Worker的线程将上述SocketChannel注册到被选择的Worker组中的NioEventLoop的Selector上,然后注册读事件(OP_READ)到Selector上。
2.关于线程池
一般的文档中在说Netty的Reactor模式的时候,都说的是线程池。其实,从源码看来,如果用默认无参构造方式构造NioEventLoop,“线程池”这个称呼其实是不确切的,为什么这么说呢?我们继续跟代码
1) 4.1.X版本
到MultithreadEventExecutorGroup类中:
这里可以看到,如果nThreads>0,说明是Reactor的多线程模式,这里children数组大小就是nThreads。
这里executor是null,所以用的是ThreadPerTaskExecutor,它是JDK的Executor接口的实现:
可以看到,其实就是用ThreadFactory新建一个线程而已。那么对于容量为nThreads的children数组来说,就是创建了这些个线程来执行任务而已,并没有真正使用jdk的线程池模型。
然后在调用newChild方法初始化每个数组元素的时候,以NioEventLoopGroup举例,它将该executor封装在构造的NioEventLoop中,构造的NioEventLoop的个数也和数组大小相同。
2) 4.0.X版本
和4.1.x版本类似,只是构造方法的形参是ThreadFactory,而不是Executor。底层其实类似,这里的newChild方法直接将ThreadFactory传入到NioEventLoop的构造方法中,然后用这个Factory构造线程去执行任务。而上面的4.1.X版本,将这个动作封装在了ThreadPerTaskExecutor中。
总结
综上,这里所说线程池的“池”,具象化是一个Executor数组,而不是JDK的线程池。但其实也无妨,因为它也包含了线程管理和复用的功能,MultithreadEventExecutorGroup的选择器会选择出EventExecutor,拿NioEventLoop举例,它的核心方法是一个死循环的run方法。同时,该NioEventLoop中的startThread方法会判断该线程的状态,如果state是已经启动过,那么也不会调用doStartThread方法了。doStartThread方法会执行executor的execute方法,该executor就是ThreadPerTaskExecutor,会创建线程并启动,所以保证doStartThread方法执行一次,也就保证了线程组的数量恒定,和JDK线程池模型的效果是一样的。
所有EventLoop的execute方法执行,都是通过先添加任务,然后再在EventLoop的死循环中取任务执行
3.给任务分配线程
因为不是所谓的jdk的线程池,所以这里也不能像使用jdk线程池那样,来了任务直接Executor.execute就万事大吉了,如上面所说,这里的线程池其实是一个数组,所以我们要从数组中选择一个ThreadPerTaskExecutor来执行任务,选择的类是EventExecutorChooser,具体实现有GenericEventExecutorChooser和PowerOfTwoEventExecutorChooser。
4.原因分析
原因的话,因为没有官方的解释,个人感觉,第一点是上面3中说的,可以自定义选取Executor的算法,在某些场景下更为高效。其次,以NioEventLoop为中心,它对应一个线程,也对应了一个任务队列,由该单线程去轮询去任务队列里取任务处理。这和JDK的线程池模型和工作方式略有不同,Netty这样封装更利于自己Reactor模式的实现。
总而言之,就是想说一下这里的线程池的含义,希望其他看源码的小伙伴在看到这里的时候,不会一头雾水到处找jdk的线程池,却发现找到的和自己设想的不太一样。
后面还会放一些个人在学习Netty源码的时候的一些类似的总结和见解,望指正,探讨,一起学习,一起成长。