• Netty核心概念(8)之Netty线程模型


    1.前言

     第7节初步学习了一下Java原本的线程池是如何工作的,以及Future的为什么能够达到其效果,这些知识对于理解本章有很大的帮助,不了解的可以先看上一节。

     Netty为什么会高效?回答就是良好的线程模型,和内存管理。在Java的NIO例子中就我将客户端的操作单独放在一个线程中处理了,这么做的原因在于如果将客户端连接串起来,后来的连接就要等前一个处理完,当然这并不意味着多线程比单线程有优势,而是在于每个客户端都需要进行读取准备好的缓存数据,再执行一些业务逻辑。如果业务逻辑耗时很久,那么顺序执行的方式没有多线程优势大。另一个方面目前多核CPU很常见了,多线程是个不错的选择。这些在第一节就说明过,也提到过NIO并不是提升了IO操作的速度,而是减少了CPU的浪费时间,这些概念不能搞混。

     本节不涉及内存管理,只介绍相关的线程模型。

    2.核心类

     上图就是我们需要关注的体系内容了,主要从EventExecutorGroup开始往下看,再上层的父接口是JDK提供的并发包内的内容,基础是线程池中可以执行周期任务的线程池服务。所以从这我们可以知道Netty可以实现周期任务,比如心跳检测。接口定义下面将逐一介绍。

    2.1 EventExecutorGroup

      isShuttingDown():是否正在关闭,或者是已经关闭。

      shutdownGracefully():优雅停机,等待所有执行中的任务执行完成,并不再接收新的任务。

      terminationFuture():返回一个该线程池管理的所有线程都terminated的时候触发的future。

      shutdown():废弃了的关闭方法,被shutdownGracefully取代。

      next():返回一个被该Group管理的EventExecutor。

      iterator():所有管理的EventExecutor的迭代器。

      submit():提交一个线程任务。

      schedule():周期执行一个任务。

     上述方法基本上是对周期线程池的一个封装,但是扩展了EventExecuotr概念,即分了若干个小组,处理事件。另外一个比较实用的就是优雅停机了。

    2.2 EventLoopGroup

     EventLoopGroup中的方法很少,其主要是和channel结合了,就多了一个将channel注册到线程池中的方法。

    2.3 EventExecutor

     EventExecutor继承自EventExecutorGroup,这个之前也提到过该类,相当于Group中的一个子集。

      next():就是找group中下一个子集

      parent():就是所属group

      inEventLoop():当前线程是否是在该子集中

      newXXX():这个是下一节内容,此处不介绍。

    2.4 EventLoop

     该接口就一个方法,就是parent();EventLoop和EventLoopGroup与EventExecutor和EventExecutorGroup是一组相似的概念。了解这些就可以了。

    3 实现细节

     EventLoop和EventLoopGroup的实现十分简单,简单看下就可以了,这里介绍几个重要的实现类。

    3.1 AbstractEventExecutor

     该类继承自上节说过的AbstractExecutorService,其最重要的是execute方法未实现。该类是对AbstractExecutorService的一个进一步加工,添加了group的概念,和不同的Future创建方法。这里不要被之前的Java线程池模型所干扰,其不一定是线程池。回到上一节线程池的介绍,最终的样子都是Execute方法决定的。

    3.2 AbstractScheduledEventExecutor

     该类是对AbstractEventExecutor的一个进一步实现,其实现了周期任务的执行。原理是内部持有一个优先队列ScheduledFutureTask。所有周期任务都添加到这个队列中,也实现了取出周期任务的方法,但是该抽象类并没有具体执行周期任务的实现。

    3.3 SingleThreadEventExecutor

     该类是对AbstractScheduledEventExecutor的一个实现,其基本上是我们最终的一个EventLoop的雏形了,很多不同协议的EventLoop都是基于它实现的。

     虽然名字叫做单线程执行器,但是其不一定是单个线程。Executor默认使用的是ThreadPerTaskExecutor,其executor会为每一个任务创建一个线程并执行,当然你也可以传入自己的executor。Queue使用的是LinkedBlockingQueue,无容量限制的任务队列。其提供了添加任务到任务队列,从任务队列中获取任务的方法。

        protected boolean runAllTasks() {
            assert inEventLoop();
            boolean fetchedAll;
            boolean ranAtLeastOne = false;
    
            do {
                fetchedAll = fetchFromScheduledTaskQueue();
                if (runAllTasksFrom(taskQueue)) {
                    ranAtLeastOne = true;
                }
            } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
    
            if (ranAtLeastOne) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
            }
            afterRunningAllTasks();
            return ranAtLeastOne;
        }
    
        protected final boolean runAllTasksFrom(Queue<Runnable> taskQueue) {
            Runnable task = pollTaskFrom(taskQueue);
            if (task == null) {
                return false;
            }
            for (;;) {
                safeExecute(task);
                task = pollTaskFrom(taskQueue);
                if (task == null) {
                    return true;
                }
            }
        }
    

     执行过程如上:1.先获取所有的周期任务,放入taskQueue;2.不断的执行taskQueue中的任务;3.afterRunningAllTasks就是一个自由发挥的方法。safeExecute就是直接执行run方法。

        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;
                    } catch (Throwable t) {
                        logger.warn("Unexpected exception from an event executor: ", t);
                    } finally {
                        for (;;) {
                            int oldState = state;
                            if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                                    SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                                break;
                            }
                        }
    
                        // Check if confirmShutdown() was called at the end of the loop.
                        if (success && gracefulShutdownStartTime == 0) {
                            logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
                                    SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must be called " +
                                    "before run() implementation terminates.");
                        }
    
                        try {
                            // Run all remaining tasks and shutdown hooks.
                            for (;;) {
                                if (confirmShutdown()) {
                                    break;
                                }
                            }
                        } finally {
                            try {
                                cleanup();
                            } finally {
                                STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                                threadLock.release();
                                if (!taskQueue.isEmpty()) {
                                    logger.warn(
                                            "An event executor terminated with " +
                                                    "non-empty task queue (" + taskQueue.size() + ')');
                                }
    
                                terminationFuture.setSuccess(null);
                            }
                        }
                    }
                }
            });
        }
    

     上面是该Executor初始化过程,run方法又是交给子类进行初始化了。

        public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
            if (quietPeriod < 0) {
                throw new IllegalArgumentException("quietPeriod: " + quietPeriod + " (expected >= 0)");
            }
            if (timeout < quietPeriod) {
                throw new IllegalArgumentException(
                        "timeout: " + timeout + " (expected >= quietPeriod (" + quietPeriod + "))");
            }
            if (unit == null) {
                throw new NullPointerException("unit");
            }
    
            if (isShuttingDown()) {
                return terminationFuture();
            }
    
            boolean inEventLoop = inEventLoop();
            boolean wakeup;
            int oldState;
            for (;;) {
                if (isShuttingDown()) {
                    return terminationFuture();
                }
                int newState;
                wakeup = true;
                oldState = state;
                if (inEventLoop) {
                    newState = ST_SHUTTING_DOWN;
                } else {
                    switch (oldState) {
                        case ST_NOT_STARTED:
                        case ST_STARTED:
                            newState = ST_SHUTTING_DOWN;
                            break;
                        default:
                            newState = oldState;
                            wakeup = false;
                    }
                }
                if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
                    break;
                }
            }
            gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
            gracefulShutdownTimeout = unit.toNanos(timeout);
    
            if (oldState == ST_NOT_STARTED) {
                try {
                    doStartThread();
                } catch (Throwable cause) {
                    STATE_UPDATER.set(this, ST_TERMINATED);
                    terminationFuture.tryFailure(cause);
    
                    if (!(cause instanceof Exception)) {
                        // Also rethrow as it may be an OOME for example
                        PlatformDependent.throwException(cause);
                    }
                    return terminationFuture;
                }
            }
    
            if (wakeup) {
                wakeup(inEventLoop);
            }
    
            return terminationFuture();
        }
    

     上面是一个优雅停机的过程,改变该Executor的状态成ST_SHUTTING_DOWN,这里要注意addTask的时候只有shutdown状态才会拒绝,所以此时这里的逻辑还不会拒绝新任务添加,然后返回了一个terminationFuture,这里不做介绍。

    3.4 SingleThreadEventLoop

     此类继承自上面讲解的SingleThreadEventExecutor,这里多了一个tailTask队列,用于每次事件循环后置任务处理,暂且不管。重要的在于很早提到了register方法,将channel注册到线程中。

        public ChannelFuture register(Channel channel) {
            return register(new DefaultChannelPromise(channel, this));
        }
    
        public ChannelFuture register(final ChannelPromise promise) {
            ObjectUtil.checkNotNull(promise, "promise");
            promise.channel().unsafe().register(this, promise);
            return promise;
        }
    

     实际上就是生成了一个DefaultChannelPromise,将channel和线程绑定,最后都放入了unsafe对象中。

    3.5 NioEventLoop

     上面讲了一些杂乱无章的内容,这里借助NioEventLoop好好梳理一下整个设计流程。NioEventLoop继承自SingleThreadEventLoop,先对之前的相关研究进行总结:

      1.EventExecutorGroup接口是继承自Java的周期任务接口,是一个事件处理器组的概念,其相关方法有:

        是否正在关闭;优雅停机;获取一个事件处理器;提交一个任务;提交一个周期任务

      2.EventExecutor接口是事件处理器,其继承自EventExecutorGroup,目的并不是说每一个事件处理器都是一个事件处理器组,而是为了复用接口定义的方法。每个处理器都应该具备优雅停机,提交任务,判断是否关闭的方法,其它方法有:

        获取处理器组;获取下一个处理器(覆盖了父接口的next方法);判断是否在事件循环内;创建Promise、ProgressivePromise、SucceededFuture、FailedFuture

      3.EventLoopGroup继承自EventExecutorGroup,更新了父接口的含义,EventExecutor的定位是处理单个事件,group就是处理事件组了。EventLoop的定位是处理一个连接的生命周期过程中的周期事件,group是多个EventLoop的集合了。这里又有一个尴尬的地方,group按照定义本不需要定义其它方法,但是由于Server端的设计(之前说过服务端的channel也是一个线程),使用的是group,所以group必须承担单个EventLoop的职责。最终添加了额外的方法:

        获取下一个EventLoop;注册channel;

      4.EventLoop,事件循环,其也是一个处理器,最终继承自EventExecutor和EventLoopGroup,方法只有一个:

        获取父事件循环组EventLoopGroup。

     上述接口光看名称很容易陷入误解,实际上定义是想将单个loop和group分离,但是实现上由于Server端包含一个服务端监听连接线程,一个客户端连接线程,其Group承担了单个的职责,所以定义了一些本该由单个执行器处理的方法,又为了复用方法,导致loop继承了group,这样看起来怪怪的,接口理解起来就混乱了。结合上面的描述,再看一遍继承图就更清楚了:

     理解了上面的设定,我们再来看看客户端的事件处理是如何设计的,即总结上诉抽象类做了哪些事情。

      1. AbstractEventExecutor:入参就一个parent,该类完成了一个基本处理:

        a.将next设置成自己(上面说过继承的group,这个操作就和group区分开了)。

        b.优雅停机调用的是带有超时的停机方案,超时为15秒

        c.覆盖了Java提供的newTask包装成FutureTask的方法,使用了自己的PromiseTask

        d.提供安全执行方法:safeExecute,直接调用的run方法

       该类是最基础的一个抽象类,基本作用就是与group在定义混乱上做了一个区分。提供了执行器与Future关联方法和一个基本的执行任务的方法。

      2.AbstractScheduledEventExecutor:入参也是一个parent,该类对AbstractEventExecutor未处理的周期任务提供了具体的完成方法:

        a.提供计算当前距服务启动的时间

        b.提供存储ScheduledFutureTask的优先队列

        c.提供了取消所有周期任务的方法

        d.提供了获取一个符合的周期任务的方法,要满足时间,并获取后移除

        e.提供了获取距最近一个周期任务的时间多久

        f.提供了移除一个周期任务的方法

        g.提供添加周期任务的方法

       该类提供了周期任务执行的一些基本方法,涉及添加周期任务,移除,获取等方法。

      3.SingleThreadEventExecutor:入参包括parent,addTaskWakesUp标志,maxPendingTasks最大任务队列数(16和io.netty.eventexecutor.maxPendingTasks(default Integer.MAX_VALUE)参数更大的那个值),executor执行器(默认是ThreadPerTaskExecutor,每个任务创建一个线程执行),taskQueue任务队列(默认是LinkedBlockingQueue),rejectedExecutionHandler拒绝任务的处理类(默认直接抛出RejectedExecutionException)。该类主要完成了一个单线程的EventExecutor的基本操作:

        a.创建一个taskQueue

        b.中断线程

        c.从任务队列中获取一个任务,takeTask连同周期任务也会获取

        d.添加任务到任务队列

        e.移除任务队列中指定任务

        f.运行所有任务,会先将周期任务存入taskQueue,再使用safeExecute方法执行任务

        g.实现了execute方法,会添加任务到任务队列,如果当前线程不是事件循环线程,开启一个线程。通过的就是持有的executor来开启的线程任务。execute方法调用了run方法,该类没有实现run方法。任务的添加都不是通过execute直接执行了,而是走的添加任务到taskQueue,由未实现的run线程来处理这些事件。

        h.优雅停机

       这样就有了一个基础的单线程模型了,开启线程,保存,取出任务的方法都有了,只有在开启线程中执行任务的run()方法还未实现。

      4.SingleThreadEventLoop:入参和SingleThreadEventExecutor一致,不同的是多了一个tailTasks。该类主要是针对netty自身的事件循环的定义来实现方法了:

        a.注册channel,实际上是生成了一个DefaultChannelPromise对象,持有了channel,和运行该channel的EventExecutor,然后将该对象交给最底层的unsafe处理。

        b.添加一个事件周期结束后执行的尾任务tailTasks

        c.执行尾任务

        d.删除指定尾任务

       该类就很简单,没有过多的内容,只是增加了一个每个事件周期后执行的任务而已。

     回顾完了,上面4个父类构建了一个基本的带定时任务,普通任务,事件循环后置任务的EventLoop,每个channel绑定了一个线程执行器,通过DefaultChannelPromise持有两者,最终交给Unsafe操作。子类只需要实现run方法,处理任务队列中的任务。下面就是重头戏NioEventLoop这个客户端的线程是如何设计的了:

        NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                     SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
            super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
            if (selectorProvider == null) {
                throw new NullPointerException("selectorProvider");
            }
            if (strategy == null) {
                throw new NullPointerException("selectStrategy");
            }
            provider = selectorProvider;
            final SelectorTuple selectorTuple = openSelector();
            selector = selectorTuple.selector;
            unwrappedSelector = selectorTuple.unwrappedSelector;
            selectStrategy = strategy;
        }
    

     调用的就是父类方法,不过在创建EventLoop的时候创建了selector,这个是NIO中也提到过的。该EventLoop是在Group中newChild创建的。

        protected void run() {
            for (;;) {
                try {
                    switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                        case SelectStrategy.CONTINUE:
                            continue;
                        case SelectStrategy.SELECT:
                            select(wakenUp.getAndSet(false));
                            if (wakenUp.get()) {
                                selector.wakeup();
                            }
                        default:
                    }
                    cancelledKeys = 0;
                    needsToSelectAgain = false;
                    final int ioRatio = this.ioRatio;
                    if (ioRatio == 100) {
                        try {
                            processSelectedKeys();
                        } finally {
                            runAllTasks();
                        }
                    } else {
                        final long ioStartTime = System.nanoTime();
                        try {
                            processSelectedKeys();
                        } finally {
                            final long ioTime = System.nanoTime() - ioStartTime;
                            runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                        }
                    }
                } catch (Throwable t) {
                    handleLoopException(t);
                }
                try {
                    if (isShuttingDown()) {
                        closeAll();
                        if (confirmShutdown()) {
                            return;
                        }
                    }
                } catch (Throwable t) {
                    handleLoopException(t);
                }
            }
        }
    

     上面是我们需要关注的run方法,该方法被单独的线程执行。通过一个策略来判断执行哪个,默认的策略是任务队列中有任务,select执行一下,返回当前的事件数,没任务返回SelectStrategy.SELECT。简单的说就是有任务就补充新到来的事件执行所有的任务,没任务就执行新到的事件。先处理IO读写任务,再处理其他任务,ioRation设置的耗时比例是IO任务占一个执行周期的百分比,默认50,意思是IO执行了50秒,其他任务也会得到50秒的执行时间。后续操作就是获取所有select的key,执行所有的任务了。这里就有一个判断如果是停机状态,就会closeAll(),之前说优雅停机的时候就是设置了一个这个标志,最后是在执行任务之后判断。processSelectedKey环节都交给unsafe类完成了,这里就挂上了handler相关触发,handler的执行也就说明都在该线程内了。

     上面的描述虽然把整个过程都关联上了,但是最主要的问题还是混乱的:如何做到一个channel创建一个线程的?上面只是说明了channel和EventExecutor是绑定在DefaultChannelPromise并交给了Unsafe类,并没有看到是如何创建线程的。而且另一个问题在于,processSelectedKey是选择了所有的key,这不是所有的channel共享了一个线程吗?

     要解决该问题要回到Bootstrap的channel建立过程:initAndRegister()方法中,通过channelFactory创建了一个channel对象,而后ChannelFuture regFuture = config().group().register(channel);主要就是观察register方法,该类是设置的线程池NioEventLoopGroup提供的方法,其继承的是MultithreadEventLoopGroup,是调用了next方法获取的EventLoop,最后接上上面channel和eventloop绑定的内容。next中获取的EventLoop早在类初始化的时候就生成了,在构造方法中MultithreadEventExecutorGroup,children就是Eventloop,next不过是挑选了一个线程池而已,默认数量是CPU核数的2倍。这个也就是前面说的,线程数量不是越多越好。

     这样我们明白了,客户端注册的时候是分配了一个线程给它。客户端并不需要多线程,但是还是继续看后面的内容:AbstractUnsafe的register方法给出了相关解答。channel持有了该EventLoop,此时线程还是未运行状态,只是有了这么一个对象而已。

                if (eventLoop.inEventLoop()) {
                    register0(promise);
                } else {
                    try {
                        eventLoop.execute(new Runnable() {
                            @Override
                            public void run() {
                                register0(promise);
                            }
                        });
                    } catch (Throwable t) {
                        logger.warn(
                                "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                                AbstractChannel.this, t);
                        closeForcibly();
                        closeFuture.setClosed();
                        safeSetFailure(promise, t);
                    }
                }
    

     这里execute方法,这个是之前我们讲过的一个方法,其会判断当前线程是否是该channel的线程,目前都没有初始化线程肯定不是,将任务放入任务队列,开启一个线程(线程处于未启动状态才会开启,否则不执行),这个设计使得execute的时候只会开启一次线程,而所有的任务都会被放入任务队列,由这个线程执行。再回到run方法,这个是channel线程执行的方法,目前每个NioEventLoop都执行了Select等方法啊,这不是处理了所有的channel的工作吗?并没有达到一个channel生命周期控制在一个线程中啊。

     这里实际上是JAVA NIO的例子带来的误解,认为必须一个线程来使用select,然后遍历事件分配线程给channel执行读写操作。实际上在Netty中不一样,Netty所有线程都在执行select并获取相关事件,但是实际上其并没有执行所有的事件。

        private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
            final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
            if (!k.isValid()) {
                final EventLoop eventLoop;
                try {
                    eventLoop = ch.eventLoop();
                } catch (Throwable ignored) {
                    return;
                }
               
                if (eventLoop != this || eventLoop == null) {
                    return;
                }
                
                unsafe.close(unsafe.voidPromise());
                return;
            }
    
            try {
                int readyOps = k.readyOps();
                
                if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                    int ops = k.interestOps();
                    ops &= ~SelectionKey.OP_CONNECT;
                    k.interestOps(ops);
    
                    unsafe.finishConnect();
                }
    
                if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                    ch.unsafe().forceFlush();
                }
    
                if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                    unsafe.read();
                }
            } catch (CancelledKeyException ignored) {
                unsafe.close(unsafe.voidPromise());
            }
    

     看这里,if (eventLoop != this || eventLoop == null) ,如果channel的eventLoop不是当前的eventLoop就不执行,这个就一小段代码,但是就直接决定了EventLoop只对自己所绑定的channel感兴趣,最终达到了只处理自己相关的任务的目的。

    3.6 NioEventLoopGroup

     该类没有太多需要说明的内容,前面已经讲解了很多。

      1.AbstractEventExecutorGroup,实现了基本的方法,所有方法都是调用next()即挑选出一个线程执行器完成的。

      2.MultithreadEventExecutorGroup,实现了一个基本的线程池,持有子线程,主要工作是初始化了子线程数组,提供了next方法。

      3.MultithreadEventLoopGroup,实现了Netty的channel线程池,提供了register方法,虽然也是调用next的register方法。

      4.NioEventLoopGroup,实现了创建子线程数组时newChild方法,所有的EventLoop都是这个方法创建的。

     group就是两个重点,一个next()挑选事件执行器,一个newChild()创建线程执行器对象。

    4.总结

     本节耗费了大量的篇幅讲解了Netty的线程模型的设计思路,主要看点如下:

      1.解释了EventLoop、EventLoopGroup、EventExecutor、EventExecutorGroup这四者之间的关系,和这么复杂混乱的继承关系的原因。

      2.解释了group是如何初始化线程,并绑定channel的,next().register()

      3.解释了eventloop为什么和channel绑定了,execute()开启线程,以及每个eventloop都在获取IO事件,但是通过channel的eventloop是否等于当前过滤掉其它的事件,只处理自己绑定的channel事件。

     由于每个线程都在获取IO事件,所以这段逻辑变的非常复杂,这也就是我之前说的写好很IO很困难。

     最后附上一张对前面所有内容总结的一个图,清醒一下头脑,从复杂的代码中脱身:

     这个图就是一个基本的执行过程图了,可能有遗漏的地方,但是大体情况如图所示。

  • 相关阅读:
    348 git远程仓库:gitee(码云)与git,git clone,git push,git pull,git remote,SSH免密码登陆及配置
    347 git分支操作:创建分支,查看分支,切换分支,创建并切换分支,删除分支,合并分支,git合并冲突
    346 git基本命令:git init,git add,git commit,git status,git log,git diff,git reset,git忽视文件
    343 git基础入门:git的安装,git config配置,git三个区
    342 版本控制系统:概述,本地版本控制系统,集中式版本控制系統,分布式版本控制系統
    341 Javascript中的Return、Return false、Return true、return 变量 【转】
    340 跨域:jsonp,jquery对于jsonp的封装,跨域资源共享(CORS)
    339 同源:同源策略的基本概念,同源策略的目的,同源策略的限制范围
    338 XMLHttpRequest 2.0
    337 模板引擎artTemplate
  • 原文地址:https://www.cnblogs.com/lighten/p/8967630.html
Copyright © 2020-2023  润新知