Netty源码分析之Selector流程

原创
2019/02/24 21:33
阅读数 616

在Netty启动后,Netty的线程池会起一个Selector线程处理IO事件和其他业务事件,下面来看下Selector流程

流程图

Selector线程是一个循环线程它一直处理IO事件和其他业务事件。这里需要说明Selector线程是处理IO事件和处理其他业务共享线程,也就是说Selector线程会按用户配置比例来处理IO接事件和其他业务事件(如channel注册事件),可能这次在执行IO事件下次如果有其他任务来了就忙其他任务去了。

源码和实现分析

Selector的主流程就是一个run()方法,源码如下:

    @Override
    protected void run() {
        for (;;) {
            //wakenUp设置成false,并获取原来wakenUp状态
            boolean oldWakenUp = wakenUp.getAndSet(false);
            try {
             //判断当前任务队列是否有任务,如果有任务直接快速select一次以便能处理到任务
                if (hasTasks()) {
                    selectNow();
                } else {
                //如果任务队列没有任务则进行阻塞select并让出cup时间片
                    select(oldWakenUp);
                //如果wakenUp是true,则中断select一次
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                }
                //处理io事件和根据占时比例配置处理任务
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    //处理IO事件
                    processSelectedKeys();
                    //处理任务
                    runAllTasks();
                } else {
                    final long ioStartTime = System.nanoTime();
 						//处理IO事件
                    processSelectedKeys();

                    final long ioTime = System.nanoTime() - ioStartTime;
                    //处理任务
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }

                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        break;
                    }
                }
            } catch (Throwable t) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }

主要有以下几个流程

  • 循环开始先把唤醒标志wakenUp设置成false,并获取原来wakenUp状态。

这里的wakenUp是控制select的中断的标志,如果有其他任务加入会中断线程的select,代码如下

    @Override
    public void execute(Runnable task) {
    
        ...
        
        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

所以这里每次循环开始都会把wakenUp标志清理掉。

  • 判断当前任务队列是否有任务,如果有任务直接快速select一次以便能处理到任务。

  • 如果任务队列没有任务则进行阻塞select并让出cup时间片。

  • 如果wakenUp是true,则中断select一次。

这里会有个迷惑,就是如果上次加入任务中断了select一次这里状态还未清理还会再中断一次,这样重复中断设计的意义是什么?看官方说法是如果没有这个操作,第一次select被中断后等待任务执行过程中的所有加入的任务都不能改变wakenUp的状态为ture,因为改变状态是用cas方式wakenUp.compareAndSet(false, true),所以下次select会出现不必要的阻塞。因此这里做了重复的唤醒。这样做在没任务加入情况下其实是浪费的所以官方称这种做法inefficient。

  • 处理io事件和根据占时比例配置处理任务

这里配置比例主要是让用户协调io事件和任务执行的时间,如果ioRatio配置100,会先执行io事件然后执行全部的任务;默认ioRatio配置50,会先执行io事件然后用io事件50%执行时间处理任务,处理任务的时间计算公式如下:

io事件处理时间 * (100 - ioRatio) / ioRatio

下面来看下select的实现:

    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            //select计数器
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            //根据定时任务计算select延迟时间
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
            		//计算select阻塞时间
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                //阻塞式select
                int selectedKeys = selector.select(timeoutMillis);
                //计时器加1
                selectCnt ++;
                //如果发现待处理io事件或老唤醒标记true或最新唤醒标记为true或队列中有任务或有定时任务,跳出循环,中断本次轮询
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    break;
                }
                //如果有线程被中断也,,跳出循环,中断本次轮询
                if (Thread.interrupted()) {
                    
                    selectCnt = 1;
                    break;
                }
                //如果的selet时间大于等于timeoutMillis,说明selet正常,计数器重归于1
                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    selectCnt = 1;
                //如果selet在timeoutMillis时间内返回次数大于配置次数说明可能触发了jdk的nio bug,则重建selector
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    rebuildSelector();
                    selector = this.selector;
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }
            
        } catch (CancelledKeyException e) {
        }
    }

select的操作是个循环,select会一直循环直到出现以下情况:

  • 发现待处理io事件。
  • 老唤醒标记true,说明处理IO接事件和其他业务事件期间加入了任务。
  • 最新唤醒标记为true,说select期间加入了任务。
  • 队列中有任务或有定时任务。
  • 触发了jdk的nio bug。

select的操作主要有以下几个流程:

  • 根据定时任务计算select延迟时间。 这里计算方式就取定时任务队列里的第一个任务(任务根据执行时间从小到大)获取执行时间加0.5毫秒,如果没有任务默认1秒加0.5毫秒。

  • 阻塞式select。

  • 如果发现待处理io事件或老唤醒标记true或最新唤醒标记为true或队列中有任务或有定时任务,跳出循环,中断本次轮询。

  • 如果的select时间大于等于timeoutMillis,说明select正常,计数器重归于1。

  • 如果select在timeoutMillis时间内返回次数大于配置次数说明可能触发了jdk的nio bug,则重建selector。 此bug会在没有IO事件发生时select立即返回,所以会造成无意义的循环最后可能导致cpu飙到100%情况,所以NIO采用计数器和重建selector方法解决这个bug

    	关于该bug的描述见 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6595055
    

    重建的过程就是先创建新的selector,将老的selector中监听key复制到新selector中,然后注册Channel到新selector中,最后关闭老的selector。

下面看下IO处理的实现

IO处理的实现就是先获取待处理的key,然后交给processSelectedKey方法去处理,我们看下processSelectedKey方法的实现

    private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0){
                unsafe.read();
                if (!ch.isOpen()) {
                    return;
                }
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                ch.unsafe().forceFlush();
            }
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

系统会根据SelectionKey处理哪种IO事件,IO事件共有4种:

  • SelectionKey.OP_READ 读事件
  • SelectionKey.OP_ACCEPT 接收事件
  • SelectionKey.OP_WRITE 写事件
  • SelectionKey.OP_CONNECT 连接事件

比如客户端链接后会触发服务端SelectionKey.OP_ACCEPT的接收事件,然后由服务端的主Channel处理,处理的过程后面会介绍。

处理完IO事件就是处理队列中的任务如果ioRatio配置成100比较简单处理方式就是一个一个执行队列里的全部任务,ioRatio非100处理比较复杂我们来下ioRatio非100的处理实现:

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次任务进行一次,超时判断,如果超时退出执行
            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;
    }

执行任务的流程也是个循环直到超时或者没任务,执行任务流程如下:

  • 将需要触发的定时任务加入到任务队列。

  • 计算任务超时时间,超时时长是根据上面说的公式计算的。

  • 执行任务。

  • 每执行64次任务进行一次超时判断,如果超时退出执行。

    这里设计64次的原因官方给的解释是每次调用nanoTime()去判断超时是耗费性能的,所以写成64,同时官方也表示硬编码成64是不太合理后面会改成可配置。

  • 取下个任务,如果没有任务也退出执行。

展开阅读全文
打赏
0
0 收藏
分享
加载中
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部