Netty实战七之EventLoop和线程模型

简单地说,线程模型指定了操做系统、编程语言、框架或者应用程序的上下文中的线程管理的关键方面。Netty的线程模型强大但又易用,而且和Netty的一向宗旨同样,旨在简化你的应用程序代码,同时最大限度地提升性能和可维护性。java

一、线程模型概述编程

线程模型肯定了代码的执行方式,因为咱们老是必须规避并发执行可能会带来的反作用,因此理解所采用的并发模型(也有单线程的线程模型)的影响很重要。缓存

由于具备多核心或多个CPU的计算机如今已经司空见惯,大多数的现代应用程序都利用了复杂的多线程处理技术以有效地利用系统资源。相比之下,在早期的Java语言中,咱们使用多线程处理的主要方式无非是按需建立和启动新的Thread来执行并发的任务单元——一种在高负载下工做得不好的原始方式。Java 5随后引入了Executor API,其线程池经过缓存和重用Thread极大地提升了性能。安全

基本的线程池化模式能够描述为:网络

——从池的空闲线程列表中选择一个Thread,而且指派它去运行一个已提交的任务(一个Runnable的说笑呢)多线程

——当任务完成时,将该Thread返回给该列表,使其可被重用。架构

下图说明了这个模型 并发

Netty实战七之EventLoop和线程模型

虽然池化和重用线程相对简单地为每一个任务都建立和销毁线程是一种进步,可是它并不能消除由上下文切换所带来的开销,其将随着线程数量的增长很快变得明显,而且在高负载下愈演愈烈。此外,仅仅因为应用程序的总体复杂性或者并发需求,在项目的声明周期内也可能会出现其余的线程相关的问题。框架

二、EventLoop接口异步

运行任务来处理在链接的生命周期内发生的事件是任何网络框架的基本功能。与之相应的编程上的构造一般被称为事件循环——一个Netty使用了Interface io.netty.channel.EventLoop来适配的术语。

如下代码说明了事件循环的基本思想,其中的每一个任务都是一个Runnable的实例。

while(!terminated){            //阻塞,直到有事件已经就绪可被运行
            List<Runnable> readyEvents = blockUntilEventReady();            for (Runnable ev:readyEvents){                //循环遍历,并处理全部的事件
                ev.run();
            }
        }

Netty的EventLoop是协同设计的一部分,它采用了两个基本的API:并发和网络编程。首先,io.netty.util.concurrent包构建在JDK的java.util.concurrent包上,用来提供线程执行器。其次,io.netty.channel包中的类,为了与Channel的事件进行交互,扩展了这些接口/类。

下图展现了生成的类层次结构

Netty实战七之EventLoop和线程模型

在这个模型中,一个EventLoop将由一个永远都不会改变的Thread驱动,同时任务(Runnable或者Callable)能够直接提交给EventLoop实现,以当即执行或者调度执行。根据配置和可用核心的不一样,可能会建立多个EventLoop实例用以优化资源的使用,而且单个EventLoop可能会被指派用于服务多个Channel。

须要注意的是,Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法,parent(),用于返回到当前EventLoop实现的实例所属的EventLoopGroup的引用。

事件/任务的执行顺序:事件和任务是以先进先出(FIFO)的顺序执行的,这样能够经过保证字节内容老是按正确的顺序被处理,消除潜在的数据损坏的可能性。

三、Netty4中的I/O和事件处理

由I/O操做触发的事件将流经安装了一个或者多个ChannelHandler的ChannelPipeline。传播这些事件的方法调用能够随后被ChannelHandler所拦截,而且能够按需地处理事件。

事件的性质一般决定了它将被如何处理,他可能将数据从网络栈中传递到你的应用程度中,或者进行逆向操做,或者执行一些大相径庭的操做。可是事件的处理逻辑必须足够的通用和灵活,以处理全部可能的用例。所以,在Netty4中,全部I/O操做和事件都由已经被分配给了EventLoop的那个Thread来处理。

四、Netty3中的I/O操做

在之前的版本中所使用的线程模型只保证了入站(以前称为上游)事件会在所谓的I/O线程(对应于Netty4中的EventLoop)中执行。全部的出站(下游)事件都由调用线程处理,其多是I/O线程也多是别的线程。开始看起来彷佛是好主意,可是已经被发现是有问题的,由于须要在ChannelHandler中对出站事件进行仔细的同步。简而言之,不可能保证多个线程不会再同一时刻尝试访问出站事件。例如,若是你经过在不一样的线程中调用Channel.write()方法,针对同一个Channel同时触发出站的事件,就会发生这种状况。

当出站事件触发了入站事件时,将会致使另外一个负面影响。当Channel.write()方法致使异常时,须要生成并触发一个exceptionCaught事件。可是在Netty3的模型中,因为这是一个入站事件,须要在调用线程中执行代码,而后将事件移交给I/O线程去执行,然而这将带来额外的上下文切换。

Netty4中所采用的线程模型,经过在同一个线程中处理某个给定的EventLoop中所产生的全部事件,解决了这个问题。这提供了一个更加简单的执行体系架构,而且消除了在多个ChannelHandler中进行同步的须要。

五、JDK的任务调度

你须要调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经链接了5分钟以后触发的任务。一个常见的用例是,发送心跳信息到远程节点,以检查链接是否仍然还活着。若是没有响应,你便知道能够关闭该Channel了。

在Java5以前,任务调度时创建在java.util.Timer类之上,其使用了一个后台Thread,而且具备与标准线程相同的限制。随后,JDK提供了java.util.concurrent包,它定义了interface ScheduledExecutorService。

虽然选择很少,可是这些预置的实现已经足够应对大多数的用例。

如下代码展现了如何使用ScheduledExecutorService来在60秒的延迟以后执行一个任务。

//建立一个其线程池具备10个线程的ScheduledExecutorService
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);        //建立一个Runnable,以供调度稍后执行
        ScheduledFuture<?> future = executorService.schedule(new Runnable() {            @Override
            public void run() {                //该任务要打印的消息
                System.out.println("60 seconds later");
            }            //调度任务在从如今开始的60秒以后执行
        },60, TimeUnit.SECONDS);
        ...        //一旦调度任务执行完成,就关闭ScheduledExecutorService以释放资源
        executorService.shutdown();

虽然ScheduledExecutorService API是直接了当的,当时在高负载下它将带来性能上的负担。

六、使用EventLoop调度任务

ScheduledExecutorService的实现具备局限性,例如,事实上做为线程池管理的一部分,将会有额外的线程建立。若是有大量任务被紧凑地调度,那么这将成为一个瓶颈。Netty经过Channel的EventLoop实现任务调度解决了这一问题。

通过60秒以后,Runnable实例将由分配给Channel的EventLoop执行。若是要调度任务以每隔60秒执行一次,请使用sheduleAtFixedRate()方法,如如下代码。

Channel ch = ...;
        ScheduledFuture<?> future = ch.eventLoop().schedule(
                //建立一个Runnable以供调度稍后执行
                new Runnable() {
                    @Override
                    public void run() {
                        //要执行的代码
                        System.out.println("60 seconds later");
                    }
                    //调度任务在从如今开始的60秒以后执行
                },60,TimeUnit.SECONDS
        );

如前面提到的,Netty的EventLoop扩展了ScheduledExecutorService,全部它提供了JDK实现可用的全部方法,包括前面的schedule()和scheduleAtFixedRate()方法。全部操做的完整列表能够在ScheduledExecutorService的Javadoc中找到。

要取消或者检查(被调度任务)执行状态,可使用每一个异步操做所返回的ScheduledFuture。

如下代码展现了一个简单的取消操做。

Channel ch = ...;
        //调度任务,并得到所返回的ScheduledFuture
        ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...);
        //some other code that runs...
        boolean mayInterruptIfRunning = false;
        //取消该任务,防止它再次运行
        future.cancel(mayInterruptIfRunning);

七、线程管理

Netty线程模型的卓越性能取决于对于当前执行的Thread的身份的肯定,也就是说,肯定它是否分配给当前Channel以及它的EventLoop的那一线程。

若是调用线程正是支撑EventLoop的线程,那么所提交的代码块将会被执行。不然,EventLoop将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。这也是任何的Thread是如何与Channel直接交互而无需在ChannelHandler中进行额外同步的。

注意,每一个EventLoop都有它本身的任务队列,独立于任何其余的EventLoop。下图展现了EventLoop用于调度任务的执行逻辑。这也是Netty线程模型的关键组成部分。

Netty实战七之EventLoop和线程模型

以前已经阐明了不要阻塞当前I/O线程的重要性。咱们再以另外一种方式重申一次:“永远不要将一个长时间运行的任务放入到执行队列中,由于它将阻塞须要在同一线程上执行的任何其余任务。”若是必需要进行阻塞调用或者执行长时间运行的任务,咱们建议使用一个专门的EventExecutor。

除了这种受限的场景,如同传输所采用的不一样的事件处理实现同样,所使用的线程模型也能够强烈地影响到排队的任务对总体系统性能的影响。

八、EventLoop线程的分配——异步传输

异步传输实现只使用了少许的EventLoop,并且在当前的线程模型中,它们可能会被多个Channel所共享,这使得能够经过尽量少许的Thread来支撑大量的Channel,而不是每一个Channel分配一个Thread。

下图显示了一个EventLoopGroup,它具备3个固定大小的EventLoop(每一个EventLoop都由一个Thread支撑)。在建立EventLoopGroup时就直接分配了EventLoop(以及支撑它们的Thread),以确保在须要时它们是可用的。
Netty实战七之EventLoop和线程模型

EventLoopGroup负责为每一个新建立的Channel分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,而且相同的EventLoop可能会被分配给多个Channel。

一旦一个Channel被分配给一个EventLoop,它将在它的整个生命周期中都是用这个EventLoop,请注意,由于它可使你从担心你的ChannelHandler实现中的线程安全和同步问题中解脱出来。

另外,须要注意的是,EventLoop的分配方式对ThreadLocal的使用的影响。由于一个EventLoop一般会被用于支撑多个Channel,因此对于全部相关联的Channel来讲,ThreadLocal都将是同样的。这使得它对于实现状态追踪等功能来讲是个糟糕的选择。然而,在一些无状态的上下文中,它仍然能够被用于在多个Channel之间共享一些重度的或者代价昂贵的对象,甚至是事件。

九、阻塞传输

用于像OIO(旧的阻塞I/O)这样的其它传输的设计略有不一样,以下图所示。
Netty实战七之EventLoop和线程模型
这里每个Channel都将被分配给一个EventLoop(以及他的Thread)。若是你开发的应用程序使用过java.io包中的阻塞I/O实现,你可能就遇到过这种模型。

可是,正如同以前同样,获得的保证是每一个Channel的I/O事件都只会被一个Thread(用于支撑该Channel的EventLoop的那个Thread)处理。这也是另外一个Netty设计一致性的例子。

相关文章
相关标签/搜索