Netty第二章 2020 3-1——Netty源码之Reactor模型部分

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源码的时候的一些类似的总结和见解,望指正,探讨,一起学习,一起成长。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值