聊天软件开发之Netty3.x VS Netty4.x的线程模型总结

Netty 3.X的I/O操作线程模型比较复杂,它的处理模型包括两部分:

    Inbound:主要包括链路建立事件、链路激活事件、读事件、I/O异常事件、链路关闭事件等;
    Outbound:主要包括写事件、连接事件、监听绑定事件、刷新事件等。

Inbound操作的主要处理流程如下:

    I/O线程(Work线程)将消息从TCP缓冲区读取到SocketChannel的接收缓冲区中;
    由I/O线程负责生成相应的事件,触发事件向上执行,调度到ChannelPipeline中;
    I/O线程调度执行ChannelPipeline中Handler链的对应方法,直到业务实现的Last Handler;
    Last Handler将消息封装成Runnable,放入到业务线程池中执行,I/O线程返回,继续读/写等I/O操作;
    业务线程池从任务队列中弹出消息,并发执行业务逻辑。


通过对Netty 3的Inbound操作进行分析我们可以看出,Inbound的Handler都是由Netty的I/O Work线程负责执行。

Outbound操作的主要处理流程如下:

    业务线程发起Channel Write操作,发送消息;
    Netty将写操作封装成写事件,触发事件向下传播;
    写事件被调度到ChannelPipeline中,由业务线程按照Handler Chain串行调用支持Downstream事件的Channel Handler;
    执行到系统最后一个ChannelHandler,将编码后的消息Push到发送队列中,业务线程返回;
    Netty的I/O线程从发送消息队列中取出消息,调用SocketChannel的write方法进行消息发送。

Netty 4.x 线程模型

相比于Netty 3.X系列版本,Netty 4.X的I/O操作线程模型比较简答。

Outbound操作的主要处理流程如下:

    I/O线程NioEventLoop从SocketChannel中读取数据报,将ByteBuf投递到ChannelPipeline,触发ChannelRead事件;
    I/O线程NioEventLoop调用ChannelHandler链,直到将消息投递到业务线程,然后I/O线程返回,继续后续的读写操作;
    业务线程调用ChannelHandlerContext.write(Object msg)方法进行消息发送;
    如果是由业务线程发起的写操作,ChannelHandlerInvoker将发送消息封装成Task,放入到I/O线程NioEventLoop的任务队列中,由NioEventLoop在循环中统一调度和执行。放入任务队列之后,业务线程返回;
    I/O线程NioEventLoop调用ChannelHandler链,进行消息发送,处理Outbound事件,直到将消息放入发送队列,然后唤醒Selector,进而执行写操作。


通过流程分析,我们发现Netty 4修改了线程模型,无论是Inbound还是Outbound操作,统一由I/O线程NioEventLoop调度执行。即时通讯聊天软件app开发可以加蔚可云的v:weikeyun24咨询

线程模型对比

在进行新老版本线程模型PK之前,首先还是要熟悉下串行化设计的理念:

我们知道当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

为了解决上述问题,Netty 4采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由I/O线程NioEventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不需要了解Netty的线程细节,这确实是个非常好的设计理念。

一个NioEventLoop聚合了一个多路复用器Selector,因此可以处理成百上千的客户端连接,Netty的处理策略是每当有一个新的客户端接入,则从NioEventLoop线程组中顺序获取一个可用的NioEventLoop,当到达数组上限之后,重新返回到0,通过这种方式,可以基本保证各个NioEventLoop的负载均衡。一个客户端连接只注册到一个NioEventLoop上,这样就避免了多个I/O线程去并发操作它。

Netty通过串行化设计理念降低了用户的开发难度,提升了处理性能。利用线程组实现了多个串行化线程水平并行执行,线程之间并没有交集,这样既可以充分利用多核提升并行处理能力,同时避免了线程上下文的切换和并发保护带来的额外性能损耗。

了解完了Netty 4的串行化设计理念之后,我们继续看Netty 3线程模型存在的问题,总结起来,它的主要问题如下:

    Inbound和Outbound实质都是I/O相关的操作,它们的线程模型竟然不统一,这给用户带来了更多的学习和使用成本;
    Outbound操作由业务线程执行,通常业务会使用线程池并行处理业务消息,这就意味着在某一个时刻会有多个业务线程同时操作ChannelHandler,我们需要对ChannelHandler进行并发保护,通常需要加锁。如果同步块的范围不当,可能会导致严重的性能瓶颈,这对开发者的技能要求非常高,降低了开发效率;
    Outbound操作过程中,例如消息编码异常,会产生Exception,它会被转换成Inbound的Exception并通知到ChannelPipeline,这就意味着业务线程发起了Inbound操作!它打破了Inbound操作由I/O线程操作的模型,如果开发者按照Inbound操作只会由一个I/O线程执行的约束进行设计,则会发生线程并发访问安全问题。由于该场景只在特定异常时发生,因此错误非常隐蔽!一旦在生产环境中发生此类线程并发问题,定位难度和成本都非常大。


讲了这么多,似乎Netty 4 完胜 Netty 3的线程模型,其实并不尽然。在特定的场景下,Netty 3的性能可能更高,就如本文第4章节所讲,如果编码和其它Outbound操作非常耗时,由多个业务线程并发执行,性能肯定高于单个NioEventLoop线程。

但是,这种性能优势不是不可逆转的,如果我们修改业务代码,将耗时的Handler操作前置,Outbound操作不做复杂业务逻辑处理,性能同样不输于Netty 3,但是考虑内存池优化、不会反复创建Event、不需要对Handler加锁等Netty 4的优化,整体性能Netty 4版本肯定会更高。

总而言之,如果用户真正熟悉并掌握了Netty 4的线程模型和功能类库,相信不仅仅开发会更加简单,性能也会更优!

就Netty 而言,掌握线程模型的重要性不亚于熟悉它的API和功能。很多时候我遇到的功能、性能等问题,都是由于缺乏对它线程模型和原理的理解导致的,结果我们就以讹传讹,认为Netty 4版本不如3好用等。不能说所有开源软件的版本升级一定都胜过老版本,就Netty而言,我认为Netty 4版本相比于老的Netty 3,确实是历史的一大进步。

猜你喜欢

转载自blog.csdn.net/wecloud1314/article/details/126966291