NioEventLoop源码分析

NioEventLoop源码分析

  • NioEventLoop源码分析
    • 1 轮询IO事件
    • 2 处理IO事件
    • 3 处理任务队列
      • 31 转移队列任务
      • 32 定期检查任务截至时间

在Netty开发中,一般异步IO事件处理采用NioEventLoopGroup作为线程池,而NioEventLoop为线程池中单个线程,作为系统任务执行单元。接下来将从源码分析Netty是如何处理任务的。

NioEventLoop线程执行入口为其run()方法,其核心业务流程为:
1. select(wakenUp.getAndSet(false));轮询IO事件
2. processSelectedKeys();处理IO事件
3. runAllTasks();处理任务队列


1.1 轮询IO事件

首先来看看轮询IO事件,其调用NioEventLoop中void select(boolean)方法;在void select(boolean),首先计算定时截至时间,在定时超时截至时间之后50ms,依旧没有select()操作,则selectNow()之后跳出循环。

int selectCnt = 0;//统计for循环次数
long currentTimeNanos = System.nanoTime();//当前系统时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);//任务截止时间
for (;;) {
       long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;               
if (timeoutMillis <= 0) {//超出截至时间50ms
            if (selectCnt == 0) {;//循环中还没有发生任何select操作
                  selector.selectNow();    
            selectCnt = 1;
            }
            break;//select之后立即返回
}
…
}

接着判断是否有任务到来,或任务被唤醒,即刻执行selectNow()并返回。

if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;    }

在既没有达到定时任务截至时间,没有任务,且没有唤醒情形下执行selector.select(timeoutMillis);并将计数selectCnt+1
执行select(timeoutMillis)再次判断当前状态,检查是否需要退出。

if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    // - Selected something,//产生IO事件
                    // - waken up by user, or//用户唤醒
                    // - the task queue has a pending task.//taskqueue添加任务
                    // - a scheduled task is ready for processing//定时任务就绪
                    break;
}
                if (Thread.interrupted()) {
                     …
                    selectCnt = 1;
                    break;
                }

接下来解决NIO epoll模型下 selctor状态异常导致cpu负载100%的bug。
在Netty中,死循环select()执行操作时,系统给定一个默认select()有效时间,在反复循环过程中,每次循环时间小于系统给定的有效时间且循环次数到达512时,系统任务进入到epool bug中,接着就进行selector重建。

long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
           // TimeUnit.MILLISECONDS.toNanos(timeoutMillis)被认为是一次有效的select()操作
                    selectCnt = 1;//操作有效将selectCnt重置
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {//当无效循环次数达到阈值512时;
                    rebuildSelector();//重建selector
                    selector = this.selector;
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }
// SELECTOR_AUTO_REBUILD_THRESHOLD为设置上限无效循环次数

重建selector核心代码,其将原selector有效key及key.attachment()转移只新建的selector上。

private void rebuildSelector0() {
        final Selector oldSelector = selector;
        final SelectorTuple newSelectorTuple;
        …
        newSelectorTuple = openSelector();
        …
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }
                int interestOps = key.interestOps();
                key.cancel();
                SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                if (a instanceof AbstractNioChannel) {
                    // Update SelectionKey
                    ((AbstractNioChannel) a).selectionKey = newKey;
                }
         …    
       selector = newSelectorTuple.selector
if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }

NioEventLoop主要执行IO时间及定时任务,但是Netty作为高并发IO框架,遵循以优先处理IO任务的原则,ioRatio负载去设置定时任务的执行时间,默认iRatio= 50(及IO时间与任务时间相等)

小结
在轮询IO事件过程中,先判断是否需要退出IO执行流程,或执行轮询IO事件按,并且解决了Epoll bug。

1.2 处理IO事件

在分析处理IO事件之前,先来说说netty在处理IO事件之前所做的优化。

private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        …
            unwrappedSelector = provider.openSelector();
       …
        if (DISABLE_KEYSET_OPTIMIZATION) { 
            return new SelectorTuple(unwrappedSelector);
        }
//默认开启优化final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction() {
            @Override
            public Object run() {
                                   return Class.forName(
                            "sun.nio.ch.SelectorImpl",//通过反射获取SelectorImpl实例
                            false,
                            PlatformDependent.getSystemClassLoader());
                }
            }
        });
       …
        final Class selectorImplClass = (Class) maybeSelectorImplClass;
        Object maybeException = AccessController.doPrivileged(new PrivilegedAction() {
            @Override
            public Object run() {
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                …  
//将SelectorImpl实例中成员selectedKeysField和publicSelectedKeysField与selectedKeySet替换
                    selectedKeysField.set(unwrappedSelector, selectedKeySet); 
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;

            }
        });

        …
        selectedKeys = selectedKeySet;//IO事件对应的selectkey
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }
 
  

上面代码的优化点在NioEventLoop 创建selector时,将其内部成员selectedKeysField和publicSelectedKeysField与selectedKeySet替换,对于selectedKeysField和publicSelectedKeysField而言,它们的数据结构为HashSet,添加的时间复杂读为o(log(n));而selectedKeySet为netty自定义类,它实现的set接口,然而底层却是基于数组的实现,它的添加时间复杂度为o(1);

 SelectedSelectionKeySet() {
        keys = new SelectionKey[1024];
    }

    @Override
    public boolean add(SelectionKey o) {
        if (o == null) {
            return false;
        }

        keys[size++] = o;
        if (size == keys.length) {
            increaseCapacity();
        }

        return true;
    }

在IO事件触发时,Selector会往底层的selectedKeysField和publicSelectedKeysField添加数据,这种方案可以降低系统运行时间复杂度。
再来看处理IO事件的逻辑。

private void processSelectedKeys() {
        if (selectedKeys != null) {//IO事件对应的的selectedkeys,该set已被优化
            processSelectedKeysOptimized();//基于系统优化处理IO事件
        } else {
            processSelectedKeysPlain(selector.selectedKeys());//非优化时间处理
        }
    }

主要分析基于set优化处理IO事件的函数(没有优化场景下逻辑大致一致)。

private void processSelectedKeysPlain(Set selectedKeys) {
       …
        Iterator i = selectedKeys.iterator();
        for (;;) {
            final SelectionKey k = i.next();
            final Object a = k.attachment();
                   …
                processSelectedKey(k, (AbstractNioChannel) a);//处理selectorkeyif (needsToSelectAgain) {//是否再次执行select()操作
                selectAgain();
                selectedKeys = selector.selectedKeys();
}
           …
             i = selectedKeys.iterator();//进入下次循环处理selectedKey相应IO事件
                }
            }
        }
    }

从上图截取源代码可以看到,IO事件处理由:执行IO事件和是否更新selectedkeys两大业务处理组成;
展开processSelectedKey(k, (AbstractNioChannel) a)代码可以看到,该代码主要进行读,写,连接等IO事件。
每次执行完IO时间之后,都会检查selectedKeys,其判断条件为needsToSelectAgain;跟着这个变量在elcipse用Ctrl+F搜索,找到该变量的操作如下段所示

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

可以看到在每次有256次key的取消,needsToSelectAgain = true;便去更新selectedKeys
小结:整个IO事件执行大体逻辑便是,循环去遍历selectedKeys,依次处理相应的IO任务并将selectedKey移除;然后判断是否需要更新selectedKeys(每当有256个key取消任务时,更新)

小结
在处理IO时间时,先对selectedKeys进行优化,然后处理相应IO事件;同时为了避免无效的轮询,在每发生256次selectedKey取消时,重新更新selectedKeys。

1.3 处理任务队列

处理任务队列的代码位于SingleThreadEventExecutor。

 protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();
        Runnable task = pollTask();
        …
        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.
            if ((runTasks & 0x3F) == 0) {//每执行64次任务检查是否超时
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }
            task = pollTask();
           …
        }
       …
        return true;
    }

1.3.1 转移队列任务

在处理任务过程中执行fetchFromScheduledTaskQueue();将scheduledTask队列取出并存入taskQueue;为什么Netty开发者要这样做????
我们先来看看这两个列的声明及初始化。
首先看scheduledTask = pollScheduledTask(nanoTime);该方法来自AbstractScheduledEventExecutor,方法返回值为该类成员变量scheduledTaskQueue,初始化过程如下代码:

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

综上可知,scheduledTaskQueue为优先队列,接着来看看taskQueue的初始化与声明:

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

taskQueue为SingleThreadEventExecutor成员变量,在构造器中调用newTaskQueue(this.maxPendingTasks)初始化;
在SingleThreadEventExecutor类成员方法中找到newTaskQueue()实现。

protected Queue newTaskQueue(int maxPendingTasks) {
        return new LinkedBlockingQueue(maxPendingTasks);
    }

初一看,哦原来taskQueue是一个阻塞队列,似乎感觉有点不妙(NioEventLoop异步执行)!!!!
接着把代码重新定位回到NioEventLoop。

@Override
    protected Queue newTaskQueue(int maxPendingTasks) {
        // This event loop never calls takeTask()
        return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue(): PlatformDependent.newMpscQueue(maxPendingTasks);
    }

在NioEventLoop重写了父类中方法,实际在NioEventLoop执行是taskQueue为newMpscQueue队列;这里才是重点:mpscQueue队列(多生产者单消费者队列)。分析到这里fetchFromScheduledTaskQueue()的工作就是将所有优先队列中数据转移至mpsc队列;Netty要这么做的理由是:为了避免在多线程执行任务队列时,在开发过程考虑同步;这违反了Netty设计核心理念,在线程执行任务时,多个线程同时将任务移交给mscp,而由当前NioEventLoop线程去执行。

1.3.2 定期检查任务截至时间

(runTasks & 0x3F) == 0,netty设计原则以优先处理IO任务,故在每从任务队列中执行64个任务,将会去检查定时截至时间是否已到,并退出执行任务队列

小结
任务队列的处理主要将任务一直mscp队列中去处理,并在每执行64个任务时,判断是否需要退出循环。

你可能感兴趣的:(Netty,java)