标签:
本章节包括:
1)线程模型总览
2)Event Loop概念和具体实现
3)任务调度
4)实现细节
简单地陈述一下,对于一个操作系统,编程语言,框架,或者应用来说,线程模型对其都是至关重要的一部分,在什么时间如何创建一个线程都会对你的代码执行有很重要的影响,所以对于开发人员而言,懂得在各种线程模型里面权衡利弊就是一个很重要的事情,是直接使用线程模型本身还是通过一些框架或者语言提供的线程框架对于开发者而言都是需要选择的
在这个章节,我们将会详细地讲解Netty的线程模型,这个模型是很强大的,且易于使用,与Netty的设计原则一样,致力于简化你的应用代码,高性能,高可用性,我们也会向你讲解我们选择这种模型的一些经验
如果你对Java并发的API有些很好的一些理解或者经验的话,你会发现这张内容很是通熟易懂,如果你对并发的一些概念是一个小白的话,或许你需要去学习一写关于这方面的知识,Brian Goetz, et al编写的《Java Concurrency in practice》这本书会是一个不错的选择
7.1 Threading model overview
在这个小节中,我们将大体上的介绍一下线程模型,然后会讨论一下Netty过去和现在的线程模型,回顾一下两种模型的各自优点和限制
在我们之前的小节中指出,一个线程会具体指定代码到底在什么时候怎么样被执行,因为我们必须防范并发线程执行产生的一些负面影响,理解一个模型的具体含义是很重要的,忽略这些细节只想获取最大的性能和收益无异于痴人说梦,残酷的现实会打败你
因为随着计算机的多核或者多CPU变得越来越普遍,现在很多应用都会采用一些比较精细的多线程技术来充分使用计算机资源,相比之下,在比较早期的JAVA的时代,我们去创建一个线程然后用这个线程去运行一个工作单元满足我们的需求,这种比较低级的模型如果运行在高负载的情况下,会有比较差的性能体验,从JDK1.5开始引入了Excutor的API,它的线程池技术通过对线程的缓存和重复利用大大地提高了应用程序的性能
一个最基本的线程池模型是这样的:
1)从线程中获取一个空闲的线程,用这个线程去执行一个实现Runnable或者Callable接口的方法
2)当这个任务完成后,这个线程会重新回到线程中,来用于重复利用
这种模式如图7.1展示:
池化使线程变得可重复利用可以减少为每个任务创建线程销毁线程带来的性能损耗,但是并没有消除上下文切换带来的损耗,这种损耗会在线程数陡增的时候会变成非常的明显,如果在高负载的情况下,会变得更加的严重,同时,与线程模型相关的一些其他问题也会在应用中慢慢体现出来,因为你的引用中会有相当复杂业务逻辑和相当高的并发需求
简而言之,多线程是复杂的,在下一个小节中,我们将会看见Netty是如何去解决这种问题的
7.2 Interface EventLoop
对于任意一个网络框架来说,在一个网络连接的整个生命周期里面,当有事件发生的时候去执行任务去处理事件是一个基本的功能,相对应的编程结构我们一般称之为线程循环,这个术语被Netty的io.netty.channel.EventLoop接口实现
下面的代码清单中展示了一个事件循环的最基本的思想,这里的每一个任务就是一个实现Runnable接口的实例
Netty的EventLoop是一个将并发和网络这两个基本的API整合在一起然后一起协作的设计,首先,Netty的io.netty.util.concurrent包是构建于java.util.concurrent的基本java包之上的用来提供线程执行器,第二,包io.netty.channel继承这些是为了获取Channel事件,具体的继承关系图是7.2所示
在这个模型中,一个EventLoop完全是由一个线程提供并且不会改变,一个实现Runnable或者Callable接口的任务可以直接提交到EventLoop来实现实时或者定时的任务实现,这个取决你的配置和可用的内核数,多个EventLoop可以被创建用来优化资源的使用,并且一个单独的EventLoop也可以为多个Channel服务
注意Netty的EventLoop继承了ScheduledExecutorService,只定义了一个parent()方法,这个方法在下面的代码段中,这个方法的目的是返回当前EventLoop实例属于的EventLoopGroup的引用
EVENT/TASK EXECUTION ORDER 事件和任务执行顺序是FIFO的,这个通过保证字节内容可以按照正确的顺序去传输来消除了数据可能存在的混乱
7.2.1 I/O and event handling in Netty 4
在第六章节中,我们详细的讲解过:通过载入一个或者多个ChannelHandler的ChannelPipeline的I/O操作被触发一个个事件,在传播事件的时候,会被一个个ChannelHandler拦截到,然后按照需求事件被得到处理
一般来说,一个事件经常是确认它自己是如何被处理的,它可能通过网络传输数据到你的应用,或许数据从你的应用传输到网络,但是事件处理逻辑必须是通用的且必须足够灵活尽可能的重复利用,因此,在Netty4中所有的操作或者事件都是被已经分配到EventLoop的线程处理的
这个模型与Netty3的线程模型是不太一样的,在下一个小节中,我们将讲解早期的Netty3的线程模型,并且分析为什么这种模型被取代了
7.2.2 I/O operations in Netty 3
在Netty3的线程模型版本中只能保证输入事件可以按照Netty4的模型处理,所有的输出模型都是被对应的调用线程处理,也许是I/O线程也许是其他的线程,这一来是看起来还是不错的,但是当你去考虑同步问题的时候你会发现这种模型是有问题的,简而言之,这是不可能保证在多线程的环境下,同一时刻是不能尝试去输出数据的,举例来说,你在不同的线程中通过调用Channel的write方法会同时触发输出事件
当一个输出事件作为一个输入事件的结果的时候也会有一些不好的影响,例如Channel的write方法发生了异常,你需要生成一个异常捕获事件,但是在Netty3中,因为这是一个输入事件,你需要在调用线程中执行你的代码,这个处理事件交给I/O线程去执行,需要额外的线程上下文切换,也会代码损耗
Netty4采用的线程模型接着了这些问题,它通过使用同一个线程去处理发生在给定的EventLoop的所有事件,这个提供了一个简单的执行架构并且消除了ChannelHandler之间的同步问题
如果你已经理解了EventLoop的规则,那么我们再来看看任务是如何被定时执行的
7.3 Task scheduling
有时候,你需要做一个定时任务,这个任务可以在稍后的时间或者周期性的被执行,举例来说,你想要先注册一个任务,然后当客户端与服务器端连接5分钟后被触发执行,一个常用的用户使用案例是发送一个心跳信息给远程端来查看当前的连接是否还存活,如果没有响应,你应该知道你可以关闭channel了
在下一个小节中,我们将向你展示如何用Java的API和Netty的API来执行定时的任务,然后讲解一下Netty的内部实现,讲讲这样设计的优点和限制
7.3.1 JDK scheduling API
在JDK1.5之前,定时任务构建于java.util.Timer之上,它使用一个后台线程的模式去完成任务,它与标准的线程一样有着共同的限制,后台,JDK提供了java.util.concurrent包,这个包定义了ScheduledExecutorService接口,表7.1展示了java.util.concurrent.Excutors相关的工厂方法
虽然没有给出太多的参数选择,但是对于大多数的使用案例来说这是非常高效的,下面的代码清单向你展示了如何使用ScheduledExecutorService来在60秒的延迟之后执行一个任务的
尽管ScheduledExecutorService的API是直白简单的,但是它可以在很好的负载情况下可以提高很好的性能体验,下一个小节中,我们将向你展示Netty如何更加高效地提供同样的功能的
7.3.2 Scheduling tasks using EventLoop
ScheduledExecutorService的实现也是有限制的,例如对于池的管理来说额外线程的创建也是一种限制,如果当大量的定时任务涌进来的时候,这将会成为一个瓶颈,Netty使用channel的EventLoop来实现这个定时任务,如下面的代码清单所示:
当60秒过去的时候,这个Runnable实例将会被分配在channel的EventLoop执行,如果想要每隔60s就执行一下这个定时任务,使用scheduleAtFixedRate方法,如下面所示:
我们之前就说过,Netty的EventLoop继承了ScheduledExecutorService,所以它能够提供JDK的原生方法,包括schedule和scheduleAtFixedRate方法,这两个方法在我们之前的代码清单里使用过,完整的代码清单可以在java的官方的ScheduledExecutorService文档中找到
如果想要取消或者检测执行的状态的话,使用每一个异步操作返回的ScheduledFuture,下面的代码清单向你展示了一个简单的取消操作
这些例子向你说明了通过Netty的一些先进的API来完成的一些高性能需求,这些性能的实现其实是依赖于底层线程模型的实现,下一个小节我们将会具体的讨论这些线程模型
7.4 Implementation
details
在这个小节中,我们将会更加详细的讲解Netty的线程模型和定时任务实现的原则,也会说明这种模型的一些缺陷
7.4.1 Thread management
Netty线程模型的高性能取决于确定执行当前线程的身份,换句话说,也就是确定当前的执行线程是否是分配给当前Channel和EventLoop的那么线程,同一个EventLoop需要在它的整个生命周期里为分配给他的Channel处理所有的事件
如果调用线程是那个分配的EventLoop,那么这个代码块将会被执行,否则,EventLoop会把这个任务推迟一点事件执行把这个任务放入到内部队列中,当EventLoop下一个处理它的事件的时候,它会执行队列中的任务,这就解释了为什么任何线程可以相互交互,并且不需要在多个ChannelHandler同步了
注意到每一个EventLoop都会有一个它自己的任务队列,独立于任何其他的EventLoop,图7.3展示了使用EventLoop执行逻辑,这对Netty的线程模型是至关重要的一部分
我们之前陈述过不阻塞当前I/O线程的重要性,我们这里用另外一种方式再来陈述一遍:永远不要把长时间运行的任务放在执行队列中,因为它会阻塞同一个线程中的其他任务的执行,如果你一定要调用阻塞任务或者长执行时间的任务,我们建议你使用一个特定的EventExecutor
撇开这样的一个限制,当前的Netty线程模型中的任务队列会很大程度上影响系统的整体性能,但可以被用来进行事件处理
7.4.2 EventLoop/thread allocation
在EventLoopGroup容器中的EventLoop可以用来为Channel中的I/O和事件服务,根据你传输类型的不同,在EventLoopGroup中的EventLoop也会进行相应类型的创建和分配
ASYNCHRONOUS
TRANSPORTS
异步实现只使用少量的EventLoop,在这种设计模型中,一个EventLoop可能会被多个Channel共享,这种模型可以是多个Channel在尽可能少的数量下的线程下正常工作,而不是给每一个Channel分配一个线程
图7.4展示了一个具有三个固定数量的EventLoop的EventLoopGroup,当EventLoopGroup创建的时候,EventLoop直接被分配好来达到当有需求来的时候,他们可以直接被使用
EventLoopGroup负责为每一个新的创建好的Channel分配一个EventLoop,在目前的实现中,使用轮询的方式来获取最平衡的分发,同样的EventLoop可以分配给多个Channel
一旦一个Channel分配到一个EventLoop,那么这个Channel在其整个的生命周期里只会使用这一个EventLoop,牢记这个特性,因为这个特性可以使你从担心线程安全和同步你的ChannelHandler这些问题中解脱出来
同样,你也要意识到EventLoop的分配也会影响到ThreadLocal的使用,因为一个EventLoop会给不止一个的Channel使用,那么对于所有的关联的Channel来说,ThreadLocal将会是一样的,这对于实现状态跟踪功能的实现来说这是一个坏的选择,但是,一个无状态的上下文对于分享一些重量昂贵的对象,例如event等来说却是有用的
BLOCKING TRANSPORTS
对于其他的阻塞传输类型例如OIO来说这个设计会有一点不同,图7,5说明了这个不同
这里一个EventLoop分配给一个Channel,如果你使用过java.io包开发过阻塞I/O应用,你也许会遇到过这种设计
但是与刚才的设计一样,这样同样保证了一个Channel的所有I/O事件可以只被同一个EventLoop上的线程处理,这是Netty的另一种一致性设计的例子,这也是为Netty的稳定性和易用性做出了卓越的贡献
7.5 Summary
在这个章节中,大体地讲解了线程模型和Netty所特有的模型,我们详细地讲解了它的性能和一致性特性
你领略到了使用EventLoop执行你的任务与执行用你的框架执行的效果是一样的,你学习了如何定时任务来做到推迟执行,在高负载的情况下可扩展的问题,如何确认一个任务是否被执行了,如何取消这个任务
这部分的知识,补充了我们对Netty框架的实现细节的学习,这将帮助我们在简化我们代码的同时最大化你应用的性能,关于线程池和并发编程的细节,我们推荐你Brian Goetz
的《Java Concurrency in practice》 的书,他的书会给你更深的理解
现在我们已经到了最兴奋的时刻,下一个章节,我们将讨论bootstrapping,配置和连接Netty各个组件的过程都是由它处理的,它将带给你应用全新的生命
Netty in Action (十七) 第七章节 EventLoop和线程模型
标签:
原文地址:http://blog.csdn.net/linuu/article/details/51166369