Netty in action—EventLoop和线程模型

来源:互联网 发布:it设备管理 编辑:程序博客网 时间:2024/06/10 17:52

线程模型概述

在本节中,我们将介绍线程模型,然后讨论Netty过去和现在的线程模型,审查每个模型的优点和局限性。
线程模型指定了代码被执行的方式。因为我们一定要防范并发执行可能的副作用,所以了解使用的模型会产生的影响很重要(也有单线程模型)。

因为具有多核或CPU的计算机是常见的,有很多应用程序采用复杂的多线程技术来有效利用
系统资源。相比之下,我们在Java早期的应用多线程方法仅仅是创建和启动新的线程,以执行并发工作单元,这是一种在高负载下工作不良的原始方法。 Java 5然后引入了了Executor API,其线程池通过线程缓存和重用大大提高了性能。

基本线程池模型可以描述为:

  • 从线程池的空闲线程列表中选出一个线程来执行一个提交的任务
  • 任务完成后,线程回到线程池并且可以被复用

如下图所示:

这里写图片描述
缓存和重用线程是创建和销毁线程的一个改进,但它不消除上下文切换的成本,随着线程数量的增加而变得明显,并且可能会造成高负载。 此外,在项目的生命周期中可能会出现其他线程相关的问题,只是因为应用程序的整体复杂性或并发性要求。

简之,多线程编程可能很复杂。
在接下来的部分我们将看看Netty是怎样简化它的

EventLoop接口

网络编程框架的基本功能是,运行任务(线程)来处理在连接期间发生的事件。相应的编程结构经常被称作一个事件循环。Netty通过接口io.netty.channel.EventLoop实现了这个结构。

一个事件循环的基本概念如下代码所示:

while (!terminated) {    //阻塞直到有准备好的事件    List<Runnable> readyEvents = blockUntilEventsReady();    for (Runnable ev: readyEvents) {        ev.run();    }}

Netty的EventLoop是协同设计(collaborative design)的一部分,它应用了两种基本的API:并发和网络编程。首先io.netty.util.concurrent包构建在JDKjava.util.concurrent之上来提供线程executors。然后,在io.netty.channel包中的与EventLoop相关的类继承了java.util.concurrent中的(Executor)类来与Channel的事件交互。类结构图如下图所示:
这里写图片描述

在这个模型中,EventLoop由一个线程驱动(这种关系是稳定的,不会切换到其他线程),
并且任务(Runnable或Callable)可以直接提交给EventLoop的实现,以便立即执行或计划执行。 取决于配置和可用的CPU数,可以创建多个EventLoop以优化资源使用,并且可以通过单个EventLoop来服务多个Channel。

注意到Netty的EventLoop继承了ScheduledExecutorService,并且EventLoop只定义了一个parent()方法。这个方法,如下面的代码片段所示,是用来返回当前EventLoop(实现类的)实例所属的EventLoopGroup的一个引用。

public interface EventLoop extends EventExecutor, EventLoopGroup {    @Override    EventLoopGroup parent();}

事件/任务执行顺序 事件和任务以先进先出的顺序执行。通过保证以正确的顺序处理字节内容来消除数据损坏的可能性。

Netty4中IO和事件的处理

IO操作触发的事件流过具有一个或多个ChannelHandler的ChannelPipeline。这些事件能被ChannelHandler拦截并且在需要的时候对事件进行处理。事件的性质通常决定如何处理,这些特性可能会使数据从网络协议栈传播到你的应用,或者相反。但事件处理逻辑必须具有通用性和灵活性使得足够处理所有可能的情况。

因此,在Netty4中,所有的IO操作和事件都由已分配给EventLoop的线程处理。

任务调度

你偶尔需要安排一个任务延后执行或周期性的执行。例如,你想注册一个任务来监听客户端已经连上5分钟了,常见的做法是发送一个心跳信息到远端来检测它是否是连接状态,如果远端没有响应,你就能关闭channel了。
下一节,你会看到如何通过Java API和Netty的EventLoop来进行任务调度。

JDK调度API

Java5以前的调度是基于java.util.Timer类的,它利用了一个后台线程同时具有与标准线程一样的局限。因此,JDK提供java.uitl.concurrent包定义了ScheduledExecutorService接口。

下面的代码显示了如何通过ScheduledExecutorService在60秒延迟后执行一个任务:

ScheduledExecutorService executor =Executors.newScheduledThreadPool(10);ScheduledFuture<?> future = executor.schedule(new Runnable() {                        @Override                        public void run() {                            System.out.println("60 seconds later");                        }                    }, 60, TimeUnit.SECONDS);...executor.shutdown();

虽然ScheduledExecutorService的API很简单,但在高负载情况下会导致开销很大。

使用EventLoop来调度任务

ScheduledExecutorService的实现有它的限制,如创建额外的线程是线程池管理的一部分,这可能会成为瓶颈。比如同时安排很多任务时。Netty通过使用Channel的EventLoop实现的调度来解决这个问题。如下面的代码所示:

Channel ch = ...ScheduledFuture<?> future = ch.eventLoop().schedule(                        new Runnable() {                        @Override                        public void run() {                            System.out.println("60 seconds later");                        }                    }, 60, TimeUnit.SECONDS);

60秒之后,channel的EventLoop会执行这个Runnable实例。
如果想每60秒执行一个任务,可以通过scheduleAtFixedRate(),如下所示:

Channel ch = ...ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(                       new Runnable() {                       @Override                       public void run() {                           System.out.println("Run every 60 seconds");                       }                   }, 60, 60, TimeUnit.Seconds);//首先60秒后执行,然后每60秒执行一次

这是我测试一下当任务本身的时间消耗超过这个时间间隔会怎样:

 ch.eventLoop().scheduleAtFixedRate(new Runnable() {                                public void run() {                                    System.out.println(new Date().toLocaleString()+" : do it...");                                    try {                                        TimeUnit.SECONDS.sleep(10);                                    } catch (InterruptedException e) {                                        e.printStackTrace();                                    }                                }                            },1,5,TimeUnit.SECONDS);

这里的时间间隔是5秒钟,但是任务需要执行10秒,那么输出会怎样:

2017-9-8 17:52:12 : do it...2017-9-8 17:52:22 : do it...2017-9-8 17:52:32 : do it...2017-9-8 17:52:42 : do it...

可看到,如果任务需要执行时间超过时间间隔,那么间隔会变成任务的执行时间。

可以通过scheduleAtFixedRate()返回的ScheduledFuture来取消执行或查看一个执行的状态。下面的代码显示了一个简单的取消操作:

ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...);// Some other code that runs...boolean mayInterruptIfRunning = false;future.cancel(mayInterruptIfRunning);//取消这个任务使它不再执行

这些例子说明了利用Netty的调度功能可以实现的性能提升。这些依赖于底层的线程模。

实现细节

线程管理

Netty的线程模型的优越性能取决于确定当前正在执行的线程的身份 也就是说,它是否是被分配
到当前Channel及其EventLoop。 (回想一下EventLoop是负责用于处理Channel生命周期中的所有事件)
这里写图片描述
如果调用Thread是EventLoop,则执行该代码块。 否则,EventLoop会调度一个任务用来延迟执行,并将其放在内部队列。当EventLoop再一次处理它的事件时,它将执行队列中的那些任务。 这解释了任何线程在不需要ChannelHandler中的同步的情况下如何直接与Channel交互。

每个EventLoop都有它自己的任务队列。上图显示了EventLoop调度任务时的执行逻辑。
我们之前声明了不阻塞当前IO线程的重要性。永远不要将一个耗时很长的任务放到执行队列,因为它会阻塞其他要在同一个线程上执行的任务。如果你必须进行阻塞调用或执行耗时很长的任务,我们建议使用一个专用的EventExecutor(DefaultEventExecutorGroup)。

EventLoop/线程分配

EventLoopGroup中包含服务IO和通channel件的EventLoops。EventLoops创建和分配的方式根据传输服务实现而有所不同。

异步的传输服务

异步实现只使用几个EventLoops(及其关联的线程),并且在当前模型中,这些可以在通道之间共享。这允许
许多Channel由尽可能少的线程服务,而不是为每个channel分配一个线程。

这里写图片描述

上图显示了一个固定大小为三个EventLoops(每个分配了一个线程)的EventLoopGroup。EventLoop(和它们的线程)被直接分配来确保它们在需要时可用。EventLoopGroup负责将EventLoop分配给每个新创建的Channel。在当前的实现中,使用循环方式实现平衡分配,并且可以将相同的EventLoop分配给多个Channel(这可能会在将来的版本中更改)。

一旦一个Channel被分配到一个EventLoop,它的整个生命周期内都会使用这个EventLoop。同样也要注意EventLoop分配实现中用到了ThreadLocal。因为一个EventLoop通常不仅为一个Channel服务,所有相关的Channel都能看到同一个ThreadLocal。这不利于实现像状态跟踪功能。然后,在无状态的环境中,对于在Channel之间分享巨大或开销大的对象甚至是事件都是很有用的。

阻塞的传输服务

其他传输服务(比如OIO)的设计和上面的有点不同,如下图所示:
这里写图片描述

每个EventLoop只分配给一个Channel。它保证了每个Channel的IO事件只会被一个线程处理。这是Netty一致性设计的另一个例子,它对Netty的可靠性和易用性贡献巨大。

阅读全文
0 0
原创粉丝点击