nettyNettynetty

自顶向下深入分析Netty(四)--EventLoop-2

2016-10-13  本文已影响2530人  Hypercube

4.4 线程

4.4.1 AbstractExecutorService

AbstractExecutorService是JDK并发包中的类,实现了ExecutorService中的submit()和invoke***()方法,关键实现是其中的newTaskFor()方法,使用FutureTask包装一个Ruannble对象和结果或者一个Callable对象。注意,这个方法是一个protected方法,子类中可以覆盖这个实现。

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

4.4.2 AbstractEventExecutor

AbstractEventExecutor继承自AbstractExecutorService并实现了EventExecutor接口,该类中只实现了一些简单的方法:

    public EventExecutor next() {
        return this;
    }    

    public boolean inEventLoop() {
        return inEventLoop(Thread.currentThread());
    }
    
    public Future<?> shutdownGracefully() {
        return shutdownGracefully(2, 15, TimeUnit.SECONDS);
    }

next()方法在线程池的讲解中已经接触过,功能是选择线程池中的一个线程,将AbstractEventExecutor看为只有一个线程的线程池,所以next()返回它本身。inEventLoop()和shutdownGracefully()方法都调用它的有参方法,我们将在其子类实现中详细介绍,这里我们先了解其功能即可。inEventLoop()的功能使判断当前线程是否是EventExecutor原生线程,shutdownGracefully()即优雅关闭。
AbstractEventExecutor类中有四个创建异步结果的方法,实现类似如下:

    public <V> Promise<V> newPromise() {
        return new DefaultPromise<V>(this);
    }

AbstractEventExecutor类覆盖了父类的newTaskFor()方法:

    @Override
    protected final <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new PromiseTask<T>(this, callable);
    }

使用Netty的PromiseTask代替JDK的FutureTask,其中的差别,我们在下一节讲述。此外,还用Netty的Future对象覆盖了subimt()方法的返回值(原本为JDK的Future).

4.4.3 AbstractScheduledEventExecutor

从名字可以看出,AbstractScheduledEventExecutor类是关于Schedule的实现。如果要调度一堆任务,那么首先要有存放任务的容器,Netty中使用队列:

    Queue<ScheduledFutureTask<?>> scheduledTaskQueue;
    
    Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
        if (scheduledTaskQueue == null) {
            scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
        }
        return scheduledTaskQueue;
    }

该调度任务队列是一个优先级队列,并使用了延迟加载。其核心的调度方法实现如下:

    <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
        if (inEventLoop()) {
            scheduledTaskQueue().add(task); // 原生线程直接向任务队列添加
        } else {
            execute(new Runnable() {    // 其他线程则提交一个添加调度任务的任务
                @Override
                public void run() {
                    scheduledTaskQueue().add(task);
                }
            });
        }
        return task;
    }

可以看出实现很简单,就是向调度任务队列中添加一个任务,为了弄明白具体的调度过程,我们需要明白ScheduledFutureTask,下面我们将详细介绍。

ScheduledFutureTask

首先看其中的静态字段和静态方法:

    // 调度任务ID生成器
    private static final AtomicLong nextTaskId = new AtomicLong();
    // 调度相对时间起点
    private static final long START_TIME = System.nanoTime();
    
    // 获取相对的当前时间
    static long nanoTime() {
        return System.nanoTime() - START_TIME;
    }

    // 获取相对的截止时间
    static long deadlineNanos(long delay) {
        return nanoTime() + delay;
    }

注意:Netty使用了相对时间调度,时间起点为ScheduledFutureTask类第一次被类加载器加载的时间。
然后我们看其中的私有字段:

    // 调度任务ID
    private final long id = nextTaskId.getAndIncrement();
    // 调度任务截止时间即到了改时间点任务将被执行
    private long deadlineNanos;
    // 任务时间间隔
    private final long periodNanos;

这里的periodNanos字段还兼有标记的功能,0--表示调度任务不重复,>0--表示按固定频率重复(at fixed rate),<0--表示按固定延迟重复(with fixed delay)。这不是一个好的设计,但也没有暴露给用户程序员,算一个折中处理。
接着我们看关键的run()方法:

    @Override
    public void run() {
        assert executor().inEventLoop();
        try {
            if (periodNanos == 0) { // 普通不重复的调度任务直接执行
                if (setUncancellableInternal()) {
                    V result = task.call();
                    setSuccessInternal(result);
                }
            } else {
                if (!isCancelled()) {   // 重复的任务可能被取消
                    task.call();
                    if (!executor().isShutdown()) { // 线程已经关闭则不再添加新任务
                        long p = periodNanos;
                        if (p > 0) {
                            deadlineNanos += p; // 按固定频率重复
                        } else {
                            deadlineNanos = nanoTime() - p; // 按固定延迟重复
                        }
                        if (!isCancelled()) {
                            Queue<ScheduledFutureTask<?>> scheduledTaskQueue =
                                    ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue;
                            assert scheduledTaskQueue != null;
                            scheduledTaskQueue.add(this); // 下一个最近的重复任务添加到任务队列
                        }
                    }
                }
            }
        } catch (Throwable cause) {
            setFailureInternal(cause);
        }
    }

代码中的注释很好的解释了一个调度任务的执行过程,可能你会对按固定延迟重复的任务有疑问,即:

    deadlineNanos = nanoTime() - p;

其中nanoTime()指当前时间(注意是相对时间),由于p是负值-p等价于:当前时间+delay时间。由于ScheduledFutureTask是添加到PriorityQueue中的对象,我们再看看其中的compareTo()方法:

    @Override
    public int compareTo(Delayed o) {
        if (this == o) {
            return 0;
        }
        ScheduledFutureTask<?> that = (ScheduledFutureTask<?>) o;
        long d = deadlineNanos() - that.deadlineNanos();
        if (d < 0) {
            return -1;
        } else if (d > 0) {
            return 1;
        } else if (id < that.id) {
            return -1;
        } else if (id == that.id) {
            throw new Error();
        } else {
            return 1;
        }
    }

从代码可以看出,优先级队列的出队顺序是:截止时间最近的先出队,如果截止时间相同则ID小的先出队。
分析完ScheduledFutureTask类,我们接着分析AbstractScheduledEventExecutor类中剩下的方法,由于其中的方法实现简单明了,不再列出代码实现,只列出其方法签名:

    // 返回当前时间(相对时间)
    protected static long nanoTime() {
        return ScheduledFutureTask.nanoTime(); // 使用ScheduledFutureTask的相对时间
    }
    // 取得并移除截止时间大于nanoTime的下一个调度任务
    protected final Runnable pollScheduledTask(long nanoTime);
    // 取得距离下一个调度任务执行的间隔时间
    protected final long nextScheduledTaskNano();
    // 取得但并不移除下一个调度任务
    final ScheduledFutureTask<?> peekScheduledTask();
    // 是否有将要执行的调度任务
    protected final boolean hasScheduledTasks();
    // 删除一个调度任务
    final void removeScheduled(final ScheduledFutureTask<?> task);

4.4.4 SingleThreadEventExecutor

SingleThreadEventExecutor类从名字可以看出,它是一个单线程的Executor实现。在介绍之前,我们先看Netty定义的线程状态:

    private static final int ST_NOT_STARTED = 1;    // 没有启动
    private static final int ST_STARTED = 2;    // 启动
    private static final int ST_SHUTTING_DOWN = 3;  // 正在关闭
    private static final int ST_SHUTDOWN = 4;   // 关闭
    private static final int ST_TERMINATED = 5; // 终止

需要注意的有两点:
(1).本类的实现中线程采用延迟启动(lazy start),只有当提交第一个任务时线程才启动,从而节省资源。
(2).当调用shutdownGracefully()时,线程状态改变为ST_SHUTTING_DOWN;调用shutdown()时,线程状态改变为ST_SHUTDOWN。
明白了线程状态,我们首先看一下类中的字段:

    private final EventExecutorGroup parent;    // 该Executor所属的线程池
    private final Queue<Runnable> taskQueue;    // 任务队列
    private final Thread thread;    // 改Executor所属的线程
    private final ThreadProperties threadProperties;    // 线程属性值
    private final Semaphore threadLock = new Semaphore(0);  // 一个信号量,注意初始值为0
    private final Set<Runnable> shutdownHooks = new LinkedHashSet<~>(); // 线程关闭钩子任务
    private final boolean addTaskWakesUp;   // 添加任务时是否唤醒线程
    private final int maxPendingTasks;  // 任务队列大小即未执行的最大任务数
    private final RejectedExecutionHandler rejectedExecutionHandler;    // 队列满时的阻止器

    private long lastExecutionTime; // 上一次执行时间

    private volatile int state = ST_NOT_STARTED;    // 线程状态,注意该字段由STATE_UPDATER修改

    // 线程终止异步结果
    private final Promise<?> terminationFuture = new DefaultPromise<Void>(
                                                            GlobalEventExecutor.INSTANCE);

关于SingleThreadEventExecutor的构造方法,我们摘选下面的关键代码:

    thread = threadFactory.newThread(() -> {
                updateLastExecutionTime();
                try {
                    SingleThreadEventExecutor.this.run();   // 这是一个模板方法
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    // shutdown
                }
    });
    taskQueue = newTaskQueue(); // 这里使用该方法是为了子类可以优化

其中使用了模板方法run(),由子类负责实现。taskQueue也由一个方法实例,主要是给子类提供一个优化的机会,关于Netty的优化,我们以后将专门讲解,这里taskQueue的默认实现是LinkedBlockingQueue。
下面我们分析一个关键方法runAllTasks(long timeoutNanos),其功能是用给定的timeoutNanos时间执行任务队列中的任务,代码如下:

    protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();  // 将调度任务队列中到期的任务移到任务队列
        Runnable task = pollTask(); // 从任务队列头部取出一个任务
        if (task == null) {
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos; // 执行截止时间
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            try {
                task.run();
            } catch (Throwable t) {
                logger.warn("A task raised an exception.", t);
            }
            runTasks ++;
            // 每执行64个任务检查时候时间已到截止时间,0x3F = 64-1
            if ((runTasks & 0x3F) == 0) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }

            task = pollTask();
            if (task == null) { // 没有任务则退出
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break;
            }
        }
        // 更新上一次执行时间
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

我们再看一下fetchFromScheduledTaskQueue()方法,它从调度任务队列取出所有到期的调度任务并加入到任务队列,除非任务队列满,代码如下:

    private boolean fetchFromScheduledTaskQueue() {
        // 等价于ScheduledFutureTask.nanoTime()
        long nanoTime = AbstractScheduledEventExecutor.nanoTime(); 
        Runnable scheduledTask  = pollScheduledTask(nanoTime);
        while (scheduledTask != null) {
            if (!taskQueue.offer(scheduledTask)) {
                // 任务队列已满,则重新添加到调度任务队列
                scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
                return false;
            }
            scheduledTask  = pollScheduledTask(nanoTime);
        }
        return true;
    }

runAllTasks()还有一个无参方法,其功能将所有到期的调度任务从调度任务队列移入任务队列,并执行任务队列中的所有任务(包括非调度任务),我们不再列出代码。
SingleThreadEventExecutor类是一个通用框架,不仅可以执行异步任务,也能执行同步任务,下面我们分析其中用于执行同步任务的关键方法takeTask(),其功能是取出任务队列头部的任务,如果没有任务则会一直阻塞,代码如下:

    protected Runnable takeTask() {
        assert inEventLoop();
        if (!(taskQueue instanceof BlockingQueue)) {  // 任务队列必须是阻塞队列
            throw new UnsupportedOperationException();
        }

        BlockingQueue<Runnable> taskQueue = (BlockingQueue<Runnable>) this.taskQueue;
        for (;;) {
            // 取得调度任务队列的头部任务,注意peek并不移除
            ScheduledFutureTask<?> scheduledTask = peekScheduledTask(); 
            if (scheduledTask == null) { // 没有调度任务
                Runnable task = null;
                try {
                    task = taskQueue.take();  // 取得并移除任务队列的头部任务,没有则阻塞
                    if (task == WAKEUP_TASK) {
                        task = null;
                    }
                } catch (InterruptedException e) {
                    // Ignore
                }
                return task;
            } else {
                long delayNanos = scheduledTask.delayNanos(); // 调度任务的到期时间间隔
                Runnable task = null;
                if (delayNanos > 0) {
                    try {   // 调度任务未到期,则从任务队列取一个任务,可能为null
                        task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS);
                    } catch (InterruptedException e) {
                        return null;
                    }
                }
                
                // 注意这里执行有两种情况:1.任务队列中没有待执行任务,2.调度任务已到期
                if (task == null) {
                    fetchFromScheduledTaskQueue();
                    task = taskQueue.poll();
                }

                if (task != null) {
                    return task;
                }
            }
        }
    }

特别关注一下15行代码,这里有一个WAKEUP_TASK,它是一个标记任务。使用这个标记任务是为了线程能正确退出,当线程需要关闭是,如果线程在take()方法上阻塞,就需要添加一个标记任务WAKEUP_TASK到任务队列,是线程从take()返回,从而正确关闭线程。

    protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop || STATE_UPDATER.get(this) == ST_SHUTTING_DOWN) {
            // 非本类原生线程或者本类原生线程需要关闭时,添加一个标记任务使线程从take()返回。
            // offer失败表明任务队列已有任务,从而线程可以从take()返回故不处理
            taskQueue.offer(WAKEUP_TASK);
        }
    }

本类覆盖了execute()方法,在这里实现了线程的延迟启动(lazy start),代码如下:

    public void execute(Runnable task) {
        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {  // 原生线程直接添加
            addTask(task);  
        } else {    // 外部线程启动线程后添加
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();   // 原生线程关闭时则阻止添加,抛出异常
            }
        }
        // 是否唤醒线程,addTaskWakesUp由构造方法配置,wakesUpForTask()可由子类覆盖,默认唤醒
        // 这里这个参数值addTaskWakesUp和其说明有出入,现在false反而唤醒?
        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

Netty线程关闭的代码较为繁琐,我们先不列出,以后专门使用一节讲述。此外,本类中其他需要说明的方法,我们列出方法签名和说明:

    // 取得并移除任务队列的头部任务,忽略WAKEUP_TASK标记任务
    protected Runnable pollTask();
    // 取得任务队列的头部任务
    protected Runnable peekTask();
    // 任务队列是否有任务即是否为空
    protected boolean hasTasks();
    // 挂起的任务数即任务队列大小
    public int pendingTasks();
    // 添加一个任务,线程关闭时抛出异常
    protected void addTask(Runnable task);
    final boolean offerTask(Runnable task);
    // 移除一个任务
    protected boolean removeTask(Runnable task);
    // 下一个调度任务到期的时间间隔
    protected long delayNanos(long currentTimeNanos);
    
    // 判断线程是否为该类的原生线程
    public boolean inEventLoop(Thread thread) {
        return thread == this.thread;
    }

4.4.5 SingleThreadEventLoop

SingleThreadEventLoop终于与Channel取得联系,其中最重要的便是register()方法,功能是将一个Channel对象注册到EventLoop上,其最终实现委托Channel对象的Unsafe对象完成,关于Unsafe我们将在下一章介绍。其代码实现如下:

    @Override
    public ChannelFuture register(Channel channel) {
        return register(channel, new DefaultChannelPromise(channel, this));
    }

    @Override
    public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
        // 代码中省略了NullPointer检查
        channel.unsafe().register(this, promise);
        return promise;
    }

该类还覆盖了父类的wakesUpForTask(Runnable task)方法,实现如下:

    @Override
    protected boolean wakesUpForTask(Runnable task) {
        return !(task instanceof NonWakeupRunnable);
    }
    
    // 标记接口,用于标记不唤醒原生线程的任务
    interface NonWakeupRunnable extends Runnable { }

4.4.6 NioEventLoop

前面铺垫了这么多,终于到了我们的目的地NioEventLoop。NioEventLoop的功能是对注册到其中的Channnel的就绪事件以及对用户提交的任务进行处理,回忆第一章关于Java NIO的讲解,NioEventLoop正是要完成第一章中所示的代码的工作。首先我们从其中的字段开始:

    Selector selector;  // NIO中的多路复用器Selector
    private SelectedSelectionKeySet selectedKeys;   // 就绪事件的键值对,优化时使用
    private final SelectorProvider provider;    // selector的工厂
    // 唤醒标记,由于select()方法会阻塞
    private final AtomicBoolean wakenUp = new AtomicBoolean(); 
    private final SelectStrategy selectStrategy; // 选择策略
    private volatile int ioRatio = 50;  // IO任务占总任务(IO+普通任务)比例
    private int cancelledKeys;  // 取消的键数目
    private boolean needsToSelectAgain; 

在讲解方法前,我们再回顾一下NioEventLoop的继承体系:
(1).JDK的AbstractExecutorService类定义了任务的提交和执行,留下了newTaskFor()方法用于子类定义执行的任务;
(2).Netty的AbstractEventExecutor类覆盖了newTaskFor()方法,使用PromiseTask表示待执行的任务;
(3).AbstractScheduledEventExecutor类将待执行的调度任务封装为ScheduledFutureTask提交给调度任务队列;
(4).SingleThreadEventExecutor类实现了任务执行器即线程,其覆盖了execute()方法,当使用execute()执行一个任务时,实质是向任务队列提交一个任务;该类中还有一个重要的模板方法run(),在这个方法中执行任务队列中的任务(调度任务队列中的待执行任务移入普通任务队列),留给子类实现;
(5).SingleThreadEventLoop类实现对Channel对象的注册。
从NioEventLoop继承体系的分析可以看出,NioEventLoop要实现的最关键方法就是基类的模板方法run()。是不是已经迫不及待了?好,我们直奔代码:

    @Override
    protected void run() {
        for (;;) { 
            try {
                // 调用select()查询是否有就绪的IO事件
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    processSelectedKeys();  // 处理就绪的IO事件
                    runAllTasks();  // 执行完任务队列中的任务
                } else {
                    final long ioStartTime = System.nanoTime();
                    processSelectedKeys();  // 处理就绪的IO事件
                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);    // 给定时间内执行任务
                }

                if (isShuttingDown()) { // 检测用户是否要终止线程
                    closeAll();
                    if (confirmShutdown()) {
                        break;
                    }
                }
            } catch (Throwable t) {
                logger.warn("Unexpected exception in the selector loop.", t);
                try {
                    Thread.sleep(1000); // 防止连续异常过度消耗CPU
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }

从代码中可以看出NioEventLoop完成了三项任务:
(1).轮训Channel选择就绪的IO事件。
(2).处理就绪的IO事件。
(3).处理任务队列中的普通任务(包含调度任务)。
其中第(3)项,我们已经在SingleThreadEventExecutor类中分析过,不再赘述。我们看代码的6-16行即第(1)项,轮询Channel选择就绪的IO事件。这里使用接口SelectStrategy是用户可以选择具体的选择策略,我们主要看默认实现:

    @Override
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }
    
    private final IntSupplier selectNowSupplier = () -> { return selectNow(); };

故默认策略是:如果有普通任务待执行,使用selectNow();否则使用select(boolean oldWakenUp)。NIO的Selector有三个select()方法,它们的区别如下:

select() 阻塞直到有一个感兴趣的IO事件就绪
select(long timeout) 与select()类似,但阻塞的最长时间为给定的timeout
selectNow() 不会阻塞,直接返回而不管是否有IO事件就绪

此外,还有一个重要的wakeUp()方法,其功能是唤醒一个阻塞在select()上的线程,使其继续运行。如果先调用了wakeUp()方法,那么下一个select()操作也会立即返回。此外,wakeUp()是一个昂贵的方法,应尽量减少其调用次数。
有了这些基础知识,我们看本类中与selec()操作有关的方法,首先看selecNow()方法:

    int selectNow() throws IOException {
        try {
            return selector.selectNow();
        } finally {
            if (wakenUp.get()) {    // wakenUp标记字段为真时,唤醒下一次select()操作
                selector.wakeup();
            }
        }
    }

实现也很简单,我们主要看select(boolean oldWakenUp)方法:

    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            // delayNanos返回的是最近的一个调度任务的到期时间,没有调度任务返回1秒
            // selectDeadLineNanos指可以进行select操作的截止时间点
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                // 四舍五入将select操作时间换算为毫秒单位
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {   // 时间不足1ms,不再进行select操作
                    if (selectCnt == 0) {   // 如果一次select操作没有进行
                        selector.selectNow();   // selecNow()之后返回
                        selectCnt = 1;
                    }
                    break;
                }

                // 此时有任务进入队列且唤醒标志为假
                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow(); // selectNow()返回,否则会耽误任务执行
                    selectCnt = 1;
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);  
                selectCnt ++;

                // 有就绪的IO事件,参数oldWakenUp为真,外部设置wakenUp为真
                // 有待执行普通任务,有待执行调度任务
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || 
                                                                        hasScheduledTasks()) {
                    break;
                }

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    selectCnt = 1;  // 截止时间已到(这里可直接break退出)
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    rebuildSelector();  // 这里是对JDK BUG的处理
                    selector = this.selector;
                    selector.selectNow(); // 重建selector之后立即selectNow()
                    selectCnt = 1;
                    break;
                }
                currentTimeNanos = time;
            }
        } catch (CancelledKeyException e) {
        }
    }

本来select操作的代码不会这么复杂,主要是由于JDK BUG导致select()方法并不阻塞而直接返回且返回值为0,从而出现空轮询使CPU完全耗尽。Netty解决的办法是:对select返回0的操作计数,如果次数大于阈值SELECTOR_AUTO_REBUILD_THRESHOLD就新建一个selector,将注册到老的selector上的channel重新注册到新的selector上。阈值SELECTOR_AUTO_REBUILD_THRESHOLD可由用户使用系统变量io.netty.selectorAutoRebuildThreshold配置,默认为512。这里注意for()循环中大量使用了break,含有break的部分才是关键操作,其他部分(其实就只有一处)是为了解决JDK BUG。
为了完全理解这段代码,我们还将讲解一下wakeUp()方法,注意其中的21行和32行代码。回忆一下SingleThreadEventExecutor的execute()方法,其最后有一个wakeUp()方法,作用是添加一个任务后指示是否需要唤醒线程。在NioEventLoop中覆盖了它的实现:

    @Override
    protected void wakeup(boolean inEventLoop) {
        // 外部线程且唤醒标记为假时唤醒
        if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
            selector.wakeup(); // 注意此时唤醒标记为真
        }
    }
    
    select(wakenUp.getAndSet(false)); // run方法调用时

当run方法调用select()方法时,每次都将唤醒标记设置为假,这样线程将阻塞在selector.select(timeoutMillis)方法上。阻塞期间如果用户使用外部线程提交一个任务,会调用上述的wakeup()方法,由于wakenUp唤醒标记为假,selector.wakeup()方法调用,线程唤醒从下一个break跳出,从而执行提交任务。阻塞期间如果外部线程提交多个任务,使用wakenUp唤醒标记使selector.wakeup()操作只执行一次,因为它是一个昂贵的操作,从而提高性能。21行代码进入if执行的前提是有任务且wakenUp唤醒标记为假,如果唤醒标记为真是什么情况呢?那说明由外部线程调用了selector.wakeup()方法,此时下一个select()操作会直接返回,继而从下一个break返回,所以也不会影响已有任务的执行。在run()方法select之后的操作还有这样两行代码:

    if (wakenUp.get()) {
        selector.wakeup();
    }

根据注释的解释是:在select(wakenUp.getAndSet(false))操作set(false)和selector.select(timeout)之间如果有外部线程将唤醒标记wakenUp设置为真且执行selector.wakeup()方法,则selector.select(timeout)的第一个操作立即返回,然后会阻塞在第二次循环的select.select(timeout)方法上,此时唤醒标记wakenUp为真从而阻止外部线程添加任务时唤醒线程,从而造成不必要的阻塞操作。(但是代码在select(timeout)之后的一行使用了hasTasks()判断,如果外部线程提交了任务也能跳出循环。所以这部分代码和注释是不是已失效?)
分析完select操作之后,我们接着分析Netty对IO事件的处理方法processSelectedKeys():

    private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());  // 使用优化
        } else {
            processSelectedKeysPlain(selector.selectedKeys());  // 普通处理
        }
    }

关于优化,我们将在专门的章节讲述,我们先看普通处理:

    private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
        if (selectedKeys.isEmpty()) {
            return; // 选择键的集合为空直接返回
        }

        Iterator<SelectionKey> i = selectedKeys.iterator();
        for (;;) {
            final SelectionKey k = i.next();
            final Object a = k.attachment();
            i.remove();

            if (a instanceof AbstractNioChannel) {  // IO事件由Netty框架处理
                processSelectedKey(k, (AbstractNioChannel) a);  
            } else {    // IO事件由用户自定义任务处理
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            if (!i.hasNext()) {
                break;
            }

            if (needsToSelectAgain) {
                selectAgain();
                selectedKeys = selector.selectedKeys();
                if (selectedKeys.isEmpty()) {
                    break;
                } else {
                    i = selectedKeys.iterator();
                }
            }
        }
    }

这一部分代码功能就是遍历选择键,其中对选择键的处理有两种方式:Netty框架处理和用户自定义处理。这两种处理方式由register()方式决定:

    // Netty框架处理
    public ChannelFuture register(final Channel channel, final ChannelPromise promise);
    // 用户自定义处理
    public void register(final SelectableChannel ch, final int interestOps, final NioTask<?> task);

注意23-31行代码,什么时候需要再次执行select()操作呢?当取消的选择键达到一定数目时,这个数目在Netty中时CLEANUP_INTERVAL,值为256。也就是每取消256个选择键,Netty重新执行一个selectAgain()操作。这个操作实现使用selector.selectNow()并将needsToSelectAgain标记设置为假。cancle()代码如下:

    void cancel(SelectionKey key) {
        key.cancel();
        cancelledKeys ++;
        if (cancelledKeys >= CLEANUP_INTERVAL) {
            cancelledKeys = 0;
            needsToSelectAgain = true;
        }
    }

接着分析最为关键的processSelectedKey()方法:

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) { // 选择键不再有效
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop(); 
            } catch (Throwable ignored) {
                return;
            }
            // channel已不再该EventLoop,直接返回
            if (eventLoop != this || eventLoop == null) {
                return; 
            }
            // channel还在EventLoop,关闭channel
            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();     // 完成连接
            }
            
            // readyOps == 0为对JDK Bug的处理, 防止死循环
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();  // 读事件以及服务端的Accept事件都抽象为read()事件
                if (!ch.isOpen()) {
                    return;
                }
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {  // 写事件
                ch.unsafe().forceFlush();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

可以看出对IO事件的具体处理,委托给NioUnsafe对象处理,由read()、forceFlush()、finishConnect()和close()方法处理具体的IO事件,具体的处理过程,我们将在分析NioUnsafe时讲解。
目前为止,我们已经讲解完了NioEventLoop实现的最关键部分,当然还有一些细节我们需要完善:

    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
        return PlatformDependent.newMpscQueue(maxPendingTasks);
    }

NioEventLoop由于不使用takeTask()方法,所以使用一个MPSC队列代替基类的LinkedBlockingQueue作为新的任务队列,大大提高了性能。如果你对MPSC(多个生产者一个消费者)队列感兴趣,可自行查看相关资料。

    @Override
    public int pendingTasks() {
        if (inEventLoop()) {
            return super.pendingTasks();
        } else {
            return submit(pendingTasksCallable).syncUninterruptibly().getNow(); // 同步等待结果
        }
    }
    
    private final Callable<Integer> pendingTasksCallable = () -> {
            return NioEventLoop.super.pendingTasks();
    };

这一部分代码是使用MPSC队列的副作用,由于MPSC只能由NioEventLoop原生线程访问,否则会发生一些意外情况,所以查询队列大小,也向任务队列提交一个任务同时同步等待结果。

    @Override
    protected Runnable pollTask() {
        Runnable task = super.pollTask();
        if (needsToSelectAgain) {
            selectAgain();
        }
        return task;
    }

NioEventLoop覆盖了pollTask()的实现,在适当时机执行selector.selectNow()操作。(由于pollTask是在执行普通任务时调用,是否有必要?就算selectNow()有结果也不能处理)
Netty作为一个优化狂魔,将优化做到了极致。回忆处理选择键的事件时,需要遍历其存储容器selectedKeySet,这是一个HashSet,迭代性能不高,那么优化。Netty使用新的SelectedSelectionKeySet代替JDK的HashSet,具体怎么实现的呢?在方法openSelector()中实现,代码不在列出,其思路是:使用反射替换这个容器。
下面我们分析SelectedSelectionKeySet,首先看字段:

    private SelectionKey[] keysA;
    private int keysASize;
    private SelectionKey[] keysB;
    private int keysBSize;
    private boolean isA = true;     // 标记字段,控制使用具体的数组

可以看出SelectedSelectionKeySet使用双数组实现,为什么要这样设计呢?
(1).使用数组提高遍历效率。
(2).遍历时使用一个数组,此时可向另一个数组添加就绪的选择键,防止ConcurrentModificationException异常发生。
再看其中的add()方法:

    @Override
    public boolean add(SelectionKey o) {
        if (o == null) {
            return false;   // 不支持null元素
        }

        if (isA) {
            int size = keysASize;
            keysA[size ++] = o; // 就绪的选择键放在末尾
            keysASize = size;
            if (size == keysA.length) {
                doubleCapacityA();  // 双倍扩充容量
            }
        } else {
            int size = keysBSize;
            keysB[size ++] = o;
            keysBSize = size;
            if (size == keysB.length) {
                doubleCapacityB();
            }
        }
        return true;
    }

从代码中可以看出,两个双数组可以视为无限容量且不支持null元素。由于双数组一个用于遍历,一个用于添加新元素,我们看关键的两个数组切换的方法,其实现也很简单,代码如下:

    SelectionKey[] flip() {
        if (isA) {
            isA = false;
            keysA[keysASize] = null;    // 最末尾元素显示置为null
            keysBSize = 0;  // B数组清空,用于添加元素
            return keysA;   // A数组返回,用于遍历
        } else {
            isA = true;
            keysB[keysBSize] = null;
            keysASize = 0;
            return keysB;
        }
    }

分析完对SelectedKeySet的优化,我们看在NioEventLoop中的使用:

    // 返回用于遍历的数组
    processSelectedKeysOptimized(selectedKeys.flip());
    
    private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
        for (int i = 0;; i ++) {
            final SelectionKey k = selectedKeys[i];
            if (k == null) {
                break;  // 注意SelectedKeySet的实现置最末尾元素为null,故必能跳出
            }
            selectedKeys[i] = null; // 设置为null,帮助GC进行回收

            final Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);  // Netty框架处理
            } else {
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);    // 用户自定义处理
            }

            if (needsToSelectAgain) {   // 有必要重新选择
                for (;;) {
                    i++;
                    if (selectedKeys[i] == null) {
                        break;
                    }
                    // 将上一次遍历集合中未处理元素置null,帮助GC回收,防止泄露
                    selectedKeys[i] = null; 
                }

                selectAgain();  // 未处理元素也将添加到数组中
                selectedKeys = this.selectedKeys.flip();    // 取出遍历数组
                i = -1; // 遍历数组索引设置为-1是因为之后将执行i++从而还是从0开始遍历
            }
        }
    }

到了这里,我们已经分析完大部分NioEventLoop的工作原理和实现,但Netty的实现远不止这些,比如全局任务执行器GlobalEventExecutor,默认执行器DefaultEventExecutor,以及其他的ThreadPerChannelEventLoop,LocalEventLoop等等,由于我们很懒,所以不再讲述。我们休整一会,然后前往下一个目的地:Netty的优雅退出机制。

上一篇下一篇

猜你喜欢

热点阅读