netty依赖_Netty源码探究2:线程模型

ec5fb0bd92913230ca22ff9d8a6860a9.png

通过对Netty事件驱动原理分析后,我们对于Netty的线程模型尚一无所知,接下来就开始分析,Netty中的线程是如何管理的,以及Netty的线程与它基于Reactor实现的事件驱动模型是如何结合的。

从哪里开始?

在分析Netty事件驱动的时候,我们注意到了EventLoop以及EventLoopGroup这两个类,这两个类中我们看到过有线程生命周期管理以及Runnable队列,因此我们就从这两个类入手找到他们的继承树,然后展开分析。

EventLoop类层次结构分析

6405cc24d3c055f2df98e7d50de67581.png
Netty版本:4.1.32.Final

依据上面的继承关系图,可以分析出如下的一些逻辑:

单向依赖关系

io.netty.channel -> io.netty.util.concurrent -> java.util.concurrent

channel这个包可以理解为封装了真正的Netty的“业务逻辑”,而concurrent则关注对Runnable任务API的定义

与JDK的concurrent一致的API风格

jdk的concurrent包中的多个接口/抽象类用于定义对Runnable任务进行管理的API,基于这样的理念,netty也有一个对应的concurrent包,这下面的一组接口/抽象类则是定义了Netty独特的对Runnable任务管理的API。打开io.netty.util.concurrent这个包去浏览其中的类,会发现这下面的核心类都是Executor、Future、Task。

Single与Multiple继承体系分支明显

从concurrent包到channel包可以明显看到,无论是Executor、EventLoop还是Thread,都是有Single和Multiple之分,并且继承体系是不同的。对于Multiple,所有接口和类的定义统一使用Group后缀。

走马观花

接下来,走马观花的看一遍这个关系图中的所有的接口和类的代码,了解下各自的作用:

io.netty.util.concurrent

  • EventExecutorGroup:继承jdk中的ScheduledExecutorService,为线程模型定义了submit、shutdown等核心API
  • EventExecutor:一个特殊的EventExecutorGroup。具备EventExecutorGroup所有核心API,同时增加了一些API用于识别当前线程是否在EventLoop(Netty事件专属线程)而非用户线程中
  • OrderedEventExecutor:用作标记用途的空接口。便于将提交的任务做排序处理。
  • AbstractEventExecutor:作为EventExecutor接口的默认实现类,更像是一种编程习惯的产物(通常使用一个抽象类实现一个接口,作为该接口的默认实现类,然后具体的实现类集成该抽象类,而非用具体实现类直接实现此接口)
  • SingleThreadEventExecutor:作为OrderedEventExecutor的默认实现类,同时封装了实现一个任务(Runnable)在单线程中执行所需的所有逻辑,如Thread管理、任务队列管理和锁等
  • AbstractEventExecutorGroup:作为EventExecutorGroup的默认实现类,其作用类似AbstractEventExecutor
  • MultithreadEventExecutorGroup:作为EventLoopGroup的默认实现类,同时维护了一个EventExecutor数组,负责管理多线程(注意这里没有使用线程池),另外还有若干对EventExecutor数组的操作API

io.netty.channel

  • EventLoopGroup:该接口定义了一系列注册Channel的API,正是这些API,将Channel与Netty的线程整合在了一起
  • EventLoop:负责处理所有来自Channel的I/O操作。一个EventLoop可以处理多个Channel的事件
  • SingleThreadEventLoop:EventLoop的默认实现类,从集成的类以及实现的接口上,非常容易推断出它的作用:在单线程中处理来自Channel的I/O操作
  • MultithreadEventLoopGroup:作为EventLoopGroup接口的默认实现类,作用同SingleThreadEventLoop,只不过会将来自Channel的I/O操作在多线程环境下处理
  • NioEventLoopGroup:作为MultithreadEventLoopGroup的一个具体实现,关注非阻塞通信逻辑的处理
  • NioEventLoop:作为SingleThreadEventLoop的实现类,在分析Netty事件驱动原理的时候,NioEventLoop是非常核心的一个类,它封装了Dispatcher的整套逻辑

小结

通过对上面API走马观花式的概览,我们阶段性总结一下,便于为下一步继续深入分析确定方向:

  • io.netty.concurrent包封装了完整的对于线程的控制逻辑,不含一点业务逻辑(在netty中主要是网络I/O处理逻辑),虽然都冠名以EventExecutor,但实际上并没有太多事件的概念在其中。下面我们想要分析和学习Netty对JDK多线程的改进和优化,可以重点分析这个包下的API
  • io.netty.channel中的EventLoop系列的接口与类,虽继承自concurrent中的相关类和接口,但实际并不关注线程的处理,对于Channel、Selector的处理才是核心

Netty的这套API虽然繁多,但设计上严格遵循自己的一套逻辑,只要搞清楚这个逻辑,再回头深究下去,会发现思路变的清晰。

接下来?

既然要研究Netty的线程模型,那就不妨从下面几个点入手把:

  • Netty中的线程的生命周期是怎样的?
  • 单线程和多线程分别在什么情况下使用?
  • Netty中的多线程是如何管理的?相对于JDK的ThreadPoolExecutor有什么优势?

Netty的单线程生命周期

线程的启动

依然从下面一段代码开始

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {

                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new PrintServerHandler());
                }
            })
            .option(ChannelOption.SO_BACKLOG, 128)
            .childOption(ChannelOption.SO_KEEPALIVE, true);

    ChannelFuture f = b.bind(port).sync();
    f.channel().closeFuture().sync();
} finally {
    workerGroup.shutdownGracefully();
    bossGroup.shutdownGracefully();
}

通过对Netty事件驱动原理的分析,我们知道:

  • bind这个方法是启动EventLoop的关键方法,因此我们从这个方法深入进去,就可以找到线程启动的相关逻辑
  • ServerBootstrap是类似Builder模式的一个工具类,链式调用的目的是构建一个Server,因此这个过程中会有一些对Thread的配置,我们关注这个构建过程就可以找到相关配置逻辑

为了搞清楚Netty中线程的生命周期,我们需要再次进入bind方法一探究竟。

这个调用链略深,首先在doBind方法中,有一个doBind0的调用,该方法源码如下:

channel.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        if (regFuture.isSuccess()) {
            channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
        } else {
            promise.setFailure(regFuture.cause());
        }
    }
});

其次,在ServerBootstrap.init中也找到了这样的一段代码:

ch.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        pipeline.addLast(new ServerBootstrapAcceptor(
                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
    }
});

其实我们关注的就是这个eventLoop().execute()方法,这应该就是线程的起点,我们继续深入:

// io.netty.util.concurrent.SingleThreadEventExecutor
@Override
public void execute(Runnable task) {
    ...
    boolean inEventLoop = inEventLoop();
    addTask(task);
    if (!inEventLoop) {
        startThread();
        ...
}

比较重要的逻辑是:

  • inEventLoop:此方法及其重要,它直接确保了SingleThreadEventExecutor的execute方法始终以单线程的方式执行传入的Runnable任务。inEventLoop()方法会先判断当前线程与持有EventLoop线程引用是否相当,不相等则启动一个新的线程,然后更新EventLoop的引用为新的线程。我们了解到一个信息:通过eventLoop().execute()执行的Runnable任务,一定是在EventLoop线程中执行了
  • addTask:将当前任务添加到队列

接着深入startThread方法,最终找到启动线程的源码:

// io.netty.util.concurrent.SingleThreadEventExecutor

private void doStartThread() {
    assert thread == null;
    executor.execute(new Runnable() {
        @Override
        public void run() {
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }
            boolean success = false;
            updateLastExecutionTime();
            try {
                SingleThreadEventExecutor.this.run();
                success = true;
            ...
        }
    });
}

首先,从doStartThread方法的第一句可以看出,该方法是期望在thread这个EventLoop线程引用为空的情况下被调用,也就是首次被调用的时候,否则就无法保证在一个SingleThreadEventExecutor实例下只有一个线程在运行的原则了。怎么保证这个方法仅被调用一次呢?此方法在我们上面分析过的execute方法中,与inEventLoop()配合来共同实现这个目的;

其次,executor.execute()不一定就会启动一个新的线程,虽然我们从后面的代码可以看出,这里一定是启动了新的线程,但为以防外一,还是要看一眼这个executor的实现。追根溯源,很容易找到他是一个io.netty.util.concurrent.ThreadPerTaskExecutor,它的execute方法是通过一个DefaultThreadFactory类型的ThreadFactory创建了一个新的线程,此Factory最终通过newThread方法确实会new一个新的Thread出来,这下我们放心了;

再次,在新建的Runnable中,调用了SingleThreadEventExecutor.this.run()方法,不同子类重写此run方法来实现任务的具体逻辑。

线程的持续运行

当SingleThreadEventExecutor.execute方法被首次调用的时候,会调用SingleThreadEventExecutor.this.run方法,在Server端的实现中,如果选用NioServerSocketChannel,那么该run方法会被NioEventLoop.run()重写,这正是我们在Netty事件驱动原理中分析的Dispatcher逻辑的所在,该方法有一个无限循环,监听来自Channel的事件并将其转发到ChannelPipeline中。这段逻辑会保持EventLoop的持续运行。

线程的优雅结束

知道了线程是怎么创建的,那么线程在什么时候停止呢?

要说停止线程,那和ExecutorService接口中的一系列shutdown方法是离不开的,那我们跟进一下shutdown方法。Netty的最新版已经将shutdown标记为不推荐,所以我们稍微看下shutdownGracefully的逻辑:io.netty.util.concurrent.SingleThreadEventExecutor#shutdownGracefully

由于这个函数的内容太多,不过阅读过后,我们知道这个函数最核心的逻辑之一是对SingleThreadEventExecutor持有的变量state的改变。该变量有下面四个值:

  • ST_NOT_STARTED:初始状态。类实例化,但为调用doStartThread之前的值
  • ST_STARTED:线程启动。调用startThread方法会更新state到该状态
  • ST_SHUTTING_DOWN:调用shutdownGracefully,首先会将state置为此状态
  • ST_SHUTDOWN:该状态在shutdown方法中用到,但是shutdown已经被标记为Deprecated,因此我们应该更多关注ST_SHUTTING_DOWN状态
  • ST_TERMINATED:这是一个当队列中所有任务执行结束之后的一个状态。可以看到,当线程进入ST_SHUTTING_DOWN之后,Netty会先等待所有任务执行结束,才将状态更改为ST_TERMINATED

这个shutdown动作是谁触发的呢?当然是用户程序了:

workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();

当调用shutdownGracefully之后,state状态会被更改为ST_SHUTTING_DOWN,接着在我们上面提到的保持EventLoop线程持续运行的io.netty.channel.nio.NioEventLoop#run方法中:

// io.netty.channel.nio.NioEventLoop

try {
    if (isShuttingDown()) {
        closeAll();
        if (confirmShutdown()) {
            return;
        }
    }
} catch (Throwable t) {
    handleLoopException(t);
}

调用isShuttingDown()方法会返回true,最终退出这个run()方法,在进行一系列扫尾工作之后,EventLoop线程停止。

但是,真正做到“优雅停止”还少了一步,那就是结束掉任务队列中的其他任务。下面我们就跟踪一下这个队列中的任务执行逻辑,看下优雅停止对于这些任务的处理逻辑是怎样的。

任务队列

在SingleThreadEventExecutor中,持有两个队列:

  • Queue<Runnable> taskQueue:所有通过eventLoop().execute()方法提交的任务都会追加到此队列
  • PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue:用于一些需要延时执行任务的场景,例如Socket连接超时之后的任务、Channel发生异常后的新事件接收等待延时等

当调用shutdownGracefully()方法之后,在随后的逻辑中,confirmShutdown()方法会被调用,紧接着会调用两个方法:

  • cancelScheduledTasks:顾名思义,取消掉所有计划任务
  • runAllTasks:以同步的方式一次性运行完taskQueue中的剩余任务

如此,我们了解到,优雅停止,会等待多有的任务(当然不包含计划任务)执行完毕后才最终停止。

这里有一个小问题,Netty是如何保证单线程持续运行的同时,还能不断处理队列中的任务呢?

这个秘密在io.netty.channel.nio.NioEventLoop.run这个方法中,有两点:

  1. select方法Netty使用的是有限等待:Selector.select(timeout),且超时的时间是下一个定时任务的到期时间与当前时间的间隔
  2. 退出状态机之后,在调用完processSelectedKeys方法后,我们会看到runAllTasks方法,并伴随一行注释:Ensure we always run tasks.

答案已经相当明显了。

Netty多线程

NioEventLoopGroup继承自MultithreadEventLoopGroup,也是我们在Demo程序中所使用的EventLoopGroup的实现类。我们来看看它是怎么构建多线程环境的:

public NioEventLoopGroup() {
    this(0);
}

这个0,代表的是线程数,想要自定义线程数,可以使用另一个构造器:

public NioEventLoopGroup(int nThreads) {
    this(nThreads, (Executor) null);
}

继续深入,看一下这个nThreads是如何产生作用的。一路跟踪,最终来到了io.netty.util.concurrent.MultithreadEventExecutorGroup#MultithreadEventExecutorGroup()

其源码如下:

// io.netty.util.concurrent.MultithreadEventExecutorGroup

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                        EventExecutorChooserFactory chooserFactory, Object... args) {
    ...
    children = new EventExecutor[nThreads];

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            children[i] = newChild(executor, args);
            success = true;
            ...
        }
    }

    chooser = chooserFactory.newChooser(children);
    ...
}

这里没有使用线程池,而是使用了EventExecutor数组,其中每一个元素都调用newChild来创建。随后创建了一个chooser,该对象是next方法的实际执行者,用于从EventExecutor数组中选择下一个元素。

接下来重点看下NioEventLoopGroup对newChild方法的实现:

// io.netty.channel.nio.NioEventLoopGroup

@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
    return new NioEventLoop(this, executor, (SelectorProvider) args[0],
        ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}

NioEventLoopGroup.newChild实际上创建的是NioEventLoop实例——我们看到在命名上Netty真的做到了见名知意,逻辑与概念统一:Group就是针对单个元素的组合,这个理念在Netty全局范围内通用——因此,NioEventLoopGroup的多线程实际是对多个NioEventLoop的管理。

多个线程之前如何切换呢?是通过定义在接口EventExecutorGroup.next方法来实现。对于单线程的情况,next返回自身。对于多线程的情况,next采用了一个DefaultEventExecutorChooserFactory的工具,当调用next方法是,该Chooser会从数组中选出一个EventExecutor元素返回。至于选择的方法,分为&操作和%操作两种,由于存在如下的一个关系:

当b=2的n次幂时有:a % b = a & (b-1)

因此当线程数为2^n时,chooser选择按位与操作来提升选择性能。更多细节可查看io.netty.util.concurrent.DefaultEventExecutorChooserFactory源码。

所以,线程数尽可能使用2的n次幂。

Netty对于多Reactor模式的实践

终于来到这一步了。在这个部分,我们要彻底搞清楚Reactor模式与线程模型是如何整合的。

我们在最初分析EventLoop的继承体系的时候,就知道在EventLoopGroup中定义了一系列API可用于将Channel与EventLoop绑定,那么究竟在哪一步进行的绑定呢?直接从EventLoopGroup的register函数出发,我们能找到以下几处调用(忽略oio、http2、embedded以及不推荐的ThreadPerChannelEventLoopGroup):

  • io.netty.bootstrap.AbstractBootstrap#initAndRegister
  • io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
  • io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)

逐个展开分析。

1、io.netty.bootstrap.AbstractBootstrap#initAndRegister

在此方法中,核心代码是:

ChannelFuture regFuture = config().group().register(channel);
  • config:返回的实例,是在创建Bootstrap或者ServerBootstrap的时候,作为成员变量在初始化之时就创建的AbstractBootstrapConfig
  • group:实际调用的是io.netty.bootstrap.AbstractBootstrap#group()方法,返回的group其实是AbstractBootstrap.group,这个group又是哪里来的?且看下面一段代码:

在ServerBootstrap中有两个group方法,可以接收1个或2个EventLoopGroup类型的参数,不过单个参数的group方法其实调用了双参的group:

@Override
public ServerBootstrap group(EventLoopGroup group) {
    return group(group, group);
}

对于双参的group,其源码如下:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);
    if (childGroup == null) {
        throw new NullPointerException("childGroup");
    }
    if (this.childGroup != null) {
        throw new IllegalStateException("childGroup set already");
    }
    this.childGroup = childGroup;
    return this;
}

由ServerBootstrap自持有childGroup,其父类AbstractServerBootstrap持有parentGroup。从这类我们可以知道,initAndRegister方法中最终获得的EventLoopGroup其实就是我们使用ServerBootstrap构建Server的时候传入的bossGroup(parentGroup)。

返回去看,绑定的 channel又是哪个?我们在对事件驱动原理分析的时候就已经知道,这个channel是我们在编写Server端代码时,通过ServerBootstrap.channel方法传入的NioServerSocketChannel。而这,是一个ServerChannel,所有的客户端的I/O请求都将通过该Channel接收并转发,这一光荣的任务交给了parentGroup来处理。

2、io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    child.pipeline().addLast(childHandler);
    ...
    try {
        childGroup.register(child).addListener(new ChannelFutureListener() {
            ...
}

该函数的作用,是在当有来自client的数据需要读取的时候,将该client对应的Channel交给workerGroup(childGroup)来处理。

3、io.netty.channel.MultithreadEventLoopGroup#register()

@Override
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

由于是在多线程模型上绑定一个channel,所以要先选择一个线程出来——调用next方法,然后调用这个EventLoop上的register。该方法最终是对io.netty.channel.SingleThreadEventLoop#register的调用:将传入的Channel与当前EventLoop线程绑定。上面的第1、第2两个步骤中对parentGroup以及childGroup上调用register,最终执行的是第3步中的register方法,将某一个Channel绑定在一个单线程上。

最后,我们需要对这个register方法再深入研究一次,因为这个方法没有表面上 看的那么简单,调用链为:

SingleThreadEventLoop.register(Channel) ->

SingleThreadEventLoop.register(ChannelPromise) ->

AbstractUnsafe.register() ->

AbstractUnsafe.register0 ->

AbstractUnsafe.doRegister

真够长的,为了节省篇幅,源码就不贴了,解释一下 这一串调用做的事情:

  1. 将Channel和EventLoop关联起来,保证Channel的后续操作在EventLoop中进行。代码在AbstractUnssafe.register中:AbstractChannel.this.eventLoop = eventLoop;
  2. 将Channel注册到Selector上。代码在AbstractNioChannel.doRegister中:javaChannel().register(eventLoop().unwrappedSelector(), 0, this);

小结

对于Server侧,Netty允许创建两个EventLoopGroup,每个都是多线程模式。其中:

  • 将ServerChannel与parentGroup绑定。parentGroup负责运行的逻辑是:接收来自客户端的连接请求,然后将客户端的Channel绑定到childGroup上;
  • 将ClientChannel与childGroup绑定。该child EventLoop负责运行的逻辑是:客户端的Channel与Server的数据传输通信逻辑的处理;
  • 每一个EventLoop可以绑定到多个Channel,而不同的Channel虽然运行在不同的线程中,但都注册到同一个Register上(详见NioEventLoop.run)

parentGroup可以和childGroup是同一个Group。

2、Netty没有使用线程池来管理多线程,而是用了一个Executor数组来管理多个线程,并且线程的个数可由用户代码决定的,默认情况下,一个EventLoopGroup的线程数是Java虚拟机的可用的处理器数量*2(Runtime.getRuntime().availableProcessors() * 2)

总结

Netty线程模型 vs Java NIO + ThreadPoolExecutor模型

在Java NIO + ThreadPoolExecutor的实现中,将EventLoop的逻辑放在主线程中进行,而将ClientChannel的后续通信操作提交给ThreadPoolExecutor作为任务来执行,这样做的问题在于,Client的数量决定了ThreadPoolExecutor的数量,当Client暴增时,会导致Thread数量暴增,最终使得服务器性能降低甚至瘫痪。

Netty的线程模型,每个客户端的Channel是固定的分配到一个线程的,而Channel与线程并非一对一,而是一个EventLoop对应多个Channel。每个EventLoop能够做到每个常住线程在保持与客户端的数据传输时,并发的处理其他任务。

Netty线程模型的缺点

由于单线程负责处理的是一批任务,那就要求每个任务都能够极快的执行完毕,否则就会影响到队列中其他任务的执行。如果一定会有一些繁重的工作要做,那就应当把这部分任务隔离到一个独立的EventLoop(Group)中去做。

Netty线程模型+Reactor设计模式架构图

结合之前对Netty的Reactor模型的理解,我们将Netty框架的架构图升级一下:

5dc958aafc32cee59bc9a5c5e931043f.png
Netty Framework Architecture, Level 2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值