Netty 线程模型详解

1,587 阅读13分钟
原文链接: www.jianshu.com

导入

在文章Netty线程模型及EventLoop详解中,已经初步分析了Netty中的线程模型以及NioEventLoop的具体实现,在分析Netty线程模型之前还分析了Reactor的多个线程模型,那篇文章的定位在于通过阅读文章,可以对线程模型有一个很好的认识,并且对Netty的线程模型也有一个初步的认识,可以说,掌握了Netty的线程模型,就相当于掌握了Netty的一大部分内容,在我看来,线程模型就是框架的指导思想,在进行其他部分的分析之前,应该首先对线程模型进行分析总结,等了解了框架的线程模型是如何的,接下来的分析就可以基于线程模型来进行,如果在不理解线程模型之前就对框架进行分析,未免会太过盲目,并且也是没有意义的,因为有可能框架整个就是单线程模型的,也可能是多线程模型的,还有可能是更为复杂的线程模型,对于Netty来说,它的线程模型的设计使得他的其他部分的设计更为安全可靠,并且省区了很多在并发环境下需要解决的线程安全问题。

为了快速导入本文,下面再来回顾一下Netty的线程模型是怎么样的。首先,Netty的线程模型类似于Reactor主从多线程模型,它有多个线程来提供接收外来请求。然后将交给多个线程来处理这些请求。最为重要的是,在Netty中,框架保证每个Channel都会被分配一个仅有一个线程的EventLoop,也就是说,Netty框架保证在一个Channel的生命周期内,只会有一个线程来负责它的事件处理,也就不会出现多个线程来同时为一个Channel处理事件的场景,也就不用去处理并发环境下的线程安全问题了。为了更为直观的理解这种线程分配模型,可以参考下面的图片:

EventLoop分配模型
EventLoop分配模型

本文将深度分析Netty是如何实现这种线程模型的,也就是说,本文的目标是试图搞明白Netty是如何保证一条Channel在其生命周期内仅由一个线程来负责其事件处理的,更为一般的,本文将对如何实现这样的线程模型给出分析,包括应该注意什么,以及如何从Netty的线程模型的设计来学习设计自己的线程模型等内容。

Netty线程模型详解

相关的类

首先,为了能顺畅的阅读后面的内容,下面列出本文分析需要涉及的几个重要的类:

  • NioEventLoop
  • SingleThreadEventExecutor
  • SingleThreadEventLoop
  • NioEventLoopGroup
  • MultithreadEventLoopGroup
  • MultithreadEventExecutorGroup

上面的列出的类是本文重点关注的类,但是还是会涉及一些其他的类,但是核心就是分析这几个类。

NioEventLoop的实现详解

NioEventLoop是Netty中非常重要的一个类,它负责处理Channel中的事件,它内部仅有一个线程来做任务处理的工作,在每个客户端链接成功之后,Netty都会对新建了的Channel进行EventLoop的分配,上文中也有类似的图片展示可以更加形象的理解这种分配模型。一个EventLoop可能会被分配给多个Channel来负责他们的事件处理,但是一个Channel的生命周期内不会出现第二个EventLoop。下面就来分析一下NioEventLoop的具体实现细节。

先来看NioEventLoop的类继承关系,它在实现上继承了SingleThreadEventLoop,而SingleThreadEventLoop又继承了SingleThreadEventExecutor,SingleThreadEventExecutor这个类是一个较为基础的底层类,它实现了Netty线程模型的单线程语义,SingleThreadEventExecutor中仅有一个线程在工作,在进行任何任务处理之前都会判断是否在分配给当前SingleThreadEventExecutor的线程中,下面来具体分析一下SingleThreadEventExecutor是如何做到这些的。下面展示了SingleThreadEventExecutor的一个构造函数:


    protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, int maxPendingTasks,
                                        RejectedExecutionHandler rejectedHandler) {
        super(parent);
        this.addTaskWakesUp = addTaskWakesUp;
        this.maxPendingTasks = Math.max(16, maxPendingTasks);
        this.executor = ObjectUtil.checkNotNull(executor, "executor");
        taskQueue = newTaskQueue(this.maxPendingTasks);
        rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
    }

可以看到,构造函数中传递了一个Executor,而这个Executor就是用来产生一个线程给当前SingleThreadEventExecutor的,那么SingleThreadEventExecutor是如何初始化它的线程的呢?看SingleThreadEventExecutor的doStartThread方法,关于Netty的事件循环开启流程,可以参考文章Netty线程模型及EventLoop详解,本文不再分析这些流程。


    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);
                        }
                    }
                }
            }
        });
    }

运行这个方法需要保证当前thread为null,也就是说还没有初始化过,如果初始化已经完成了,那么该方法是不会运行的。该方法调用了Executor的execute方法来提交了一个任务,而我们知道,Executor.execute提交的内容会在一个新的线程中执行,所以,在这里SingleThreadEventExecutor正式获得了一个新的线程,并且将这个线程存储了起来,那这个线程存储起来有什么用呢?其实只有一个作用,在这之前,我们需要明白一件事情,操作系统会给每个线程分配一小段CPU时间,得到调度的线程可以占有一段时间的CPU时间,而时间用完了就需要再次排队等候调度,SingleThreadEventExecutor将分配给他的线程存储起来,只是作为一种标志,在进行任务执行之前,它都会进行一次判断,当前线程是否处于分配给自己的线程之中,如果不在,那么就不能执行。当然这种管理是在存在多个SingleThreadEventExecutor的时候进行的,在当个SingleThreadEventExecutor内部,如果在执行任务之前判断发现当前线程不在分配给自己的线程之中,那么只有一种可能,那就是当前的SingleThreadEventExecutor还没有被分配线程,那么就执行上面提到的startThread方法来分配一个线程给自己。

每个SingleThreadEventExecutor仅有一个线程来进行事件处理,而每个SingleThreadEventExecutor包含了一个任务队列,所有需要改SingleThreadEventExecutor处理的任务都需要添加到该任务队列中去,添加的任务会在事件循环的过程中被不断消费,下面来看一下一个任务时如何被SingleThreadEventExecutor执行的。

首先,下面的代码展示了SingleThreadEventExecutor的任务队列:


 private final Queue<Runnable> taskQueue;
 
 taskQueue = newTaskQueue(this.maxPendingTasks);
 
 protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
     return new LinkedBlockingQueue<Runnable>(maxPendingTasks);
 } 

可以看到,SingleThreadEventExecutor的任务队列是一个LinkedBlockingQueue类型的阻塞队列,关于阻塞队列的分析,可以参考文章Java阻塞队列详解,该文章详细分析了java中的阻塞队列的实现细节,包括LinkedBlockingQueue的分析也可以在该文章中找到。

下面来看一个任务时如何被添加到该任务队列中去的:


    protected void addTask(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (!offerTask(task)) {
            reject(task);
        }
    }

    final boolean offerTask(Runnable task) {
        if (isShutdown()) {
            reject();
        }
        return taskQueue.offer(task);
    }
    

首先会使用addTask来进行,然后会调用offerTask来讲任务实际添加到阻塞队列中去。在Netty中,在什么时候会添加一个任务到SingleThreadEventExecutor的阻塞队列中呢?其实很好猜测,Netty使用一个PipeLine来进行事件传播,并且我们知道Netty是一种基于NI/O的网络框架,那免不了会进行一些I/O操作,比如从Channel中读取数据,或者向Channel中写一些数据传递到对端等,这些都是事件,而这些事件的处理以及传播,都需要在SingleThreadEventExecutor来处理,而每个Channel都会被分配一个SingleThreadEventExecutor来进行这些事件的处理,所以,在一个Channel上发生了某种I/O事件的时候,Netty就会将这种事件包装成一个任务,然后添加到与该Channel对应的SingleThreadEventExecutor的任务队列中去。

上面分析了添加任务,下面来分析这些添加的任务时如何被SingleThreadEventExecutor的线程消费掉的。


    protected Runnable pollTask() {
        assert inEventLoop();
        return pollTaskFrom(taskQueue);
    }

    protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
        for (;;) {
            Runnable task = taskQueue.poll();
            if (task == WAKEUP_TASK) {
                continue;
            }
            return task;
        }
    }

消费任务首先要调用方法pollTask来获取一个任务,而pollTask会调用pollTaskFrom来从SingleThreadEventExecutor的任务队列中获取一个需要执行的任务。那Netty在什么时候会通过上面的方法来获取一个任务来执行呢?看下面的方法:


 protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();
        Runnable task = pollTask();
        if (task == null) {
            afterRunningAllTasks();
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            safeExecute(task);

            runTasks ++;

            // Check timeout every 64 tasks because nanoTime() is relatively expensive.
            // XXX: Hard-coded value - will make it configurable if it is really a problem.
            if ((runTasks & 0x3F) == 0) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }

            task = pollTask();
            if (task == null) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break;
            }
        }

        afterRunningAllTasks();
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

上面这个方法会在EventLoop中的事件循环方法run中调用,也就是说,每次事件循环,Netty都有可能来执行这些提交的任务。在方法runAllTasks中可以看到,在获取到一个任务之后,会调用safeExecute方法来进行任务执行:


    protected static void safeExecute(Runnable task) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception. Task: {}", task, t);
        }
    }

这里面还有一个问题,runAllTasks方法中执行任务的时间是受限的,从事件循环方法run中可以看到,Netty会优先执行方法processSelectedKeys的,那这个方法是干嘛的呢?下面来追踪一下:


    private void processSelectedKeysOptimized() {
        for (int i = 0; i < selectedKeys.size; ++i) {
            final SelectionKey k = selectedKeys.keys[i];
            // null out entry in the array to allow to have it GC'ed once the Channel close
            // See https://github.com/netty/netty/issues/2363
            selectedKeys.keys[i] = null;

            final Object a = k.attachment();

            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            if (needsToSelectAgain) {
                // null out entries in the array to allow to have it GC'ed once the Channel close
                // See https://github.com/netty/netty/issues/2363
                selectedKeys.reset(i + 1);

                selectAgain();
                i = -1;
            }
        }
    }


    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();
            }

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                ch.unsafe().forceFlush();
            }
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }


看到最后可以发现,Netty将优先处理的是I/O读写事件,也就是在Channel上的read/write等事件将被优化处理,而添加到阻塞任务队列中的任务也会保证被调度执行,但是什么时候被调度执行就不一定了。那现在需要重新考虑一下,到底什么类型的任务会被添加到阻塞队列中呢?换句话说,Netty会将什么类型的事件标记为不那么重要呢?下面来跟踪一下pollTask方法的调用链路:



    ==================================
    AbstractChannelHandlerContext
    ==================================

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }


    ==================================
    AbstractChannelHandlerContext
    ==================================
    
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(), msg);
        return this;
    }
    
    =================================
    ChannelInboundHandlerAdapter
    =================================
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }
        

我们要想对网络数据进行操作,可以继承ChannelInboundHandlerAdapter来处理入站数据,那么执行我们的任务处理的任务就会被添加到阻塞任务队列中去,在事件循环中被调度执行。也就是说,在Netty中,优先执行底层的I/O事件,然后对从底层传递上来的数据的处理会被添加到任务队列中调度执行。

NioEventLoopGroup的实现详解

上文中分析了SingleThreadEventExecutor是如何保证在一个线程中执行任务的,并且分析了任务被添加的过程和执行的过程,下面来分析一下Netty是如何来管理这些SingleThreadEventExecutor的(使用SingleThreadEventExecutor来代表EventLoop是可行的,你可以将SingleThreadEventExecutor连接为EventLoop,毕竟SingleThreadEventExecutor实现了EventLoop的核心内容,而EventLoop又是SingleThreadEventExecutor的子类,这样理解起来也是没有问题的)。Netty中使用NioEventLoopGroup来管理一组NioEventLoop,服务端会为每一个新建立的Channel分配一个NioEventLoop,也就是会分配一个SingleThreadEventExecutor给每个新建立的Channel,来负责处理它的事件。而NioEventLoopGroup底层核心的类为MultithreadEventExecutorGroup,由这个类管理者多个EventLoop,在需要分配一个EventLoop的时候也是通过MultithreadEventExecutorGroup的方法来进行的,下面来分析一下,MultithreadEventExecutorGroup是如何初始化一组EventLoop的,又是如何将他们分配出去的。

首先,来看下面的构造函数:


protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }

        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }

        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                if (!success) {
                    for (int j = 0; j < i; j ++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j ++) {
                        EventExecutor e = children[j];
                        try {
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }

        chooser = chooserFactory.newChooser(children);

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (terminatedChildren.incrementAndGet() == children.length) {
                    terminationFuture.setSuccess(null);
                }
            }
        };

        for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

这个构造函数较为复杂,他要做的事情是初始化一组EventLoop,这是该构造函数的核心,children数组就是他需要初始化并且管理的EventLoop数组,下面来看一下是MultithreadEventExecutorGroup是如何生成一个新的EventLoop的:


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

那它又是如何分配一个EventLoop给一个Channel的呢?使用了EventExecutorChooser这个类来作为一个选择器,它有两种不同的选择策略,下面来具体看一下:


    public EventExecutorChooser newChooser(EventExecutor[] executors) {
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }

    private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }

    private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }
    }

    private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            return executors[Math.abs(idx.getAndIncrement() % executors.length)];
        }
    }

看代码就可以很清晰的理解它的两种分配策略,第一种是PowerOfTwoEventExecutorChooser,第二种是GenericEventExecutorChooser,选择一个EventLoop是通过调用方法next来进行的,比如GenericEventExecutorChooser,它的next方法表达的意思就是轮询来获取一个EventLoop,从这里就可以看出,一个EventLoop有可能被分配多次,但是,还是要再次说明,Netty保证每个Channel的生命周期内不会出现第二个EventLoop来处理其Channel上的事件,这才是最为核心的地方。

本文较为详细的分析了Netty的线程模型,以及它的实现细节,本文可以作为对Netty线程模型及EventLoop详解的补充内容,当然,对Netty线程模型以及相关内容的分析还需要再深入一些,涉及这部分的内容也会不断进行补充,还有一些不明确的,或者理解有误的地方会在未来进行更正补充。