NioEventLoop源码分析

来源:互联网 发布:淘宝订单编号查询系统 编辑:程序博客网 时间:2024/06/08 13:48

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<Object>() {            @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<Object>() {            @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<SelectionKey> selectedKeys) {       …        Iterator<SelectionKey> 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<ScheduledFutureTask<?>>();        }        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<Runnable> newTaskQueue(int maxPendingTasks) {        return new LinkedBlockingQueue<Runnable>(maxPendingTasks);    }

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

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

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

1.3.2 定期检查任务截至时间

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

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

原创粉丝点击