• Netty NioEventLoop 源码解析(运行相关)


    Netty - NioEventLoop 源码解析(启动相关)

    NioEventLoop的构造方法这里就不说了,在上一篇种仔细介绍过。

    这里再来回顾下他的 继承体系:

    由上图可看出, 其继承了 ScheduledExecutorService 调度线程池接口

    也就是说 该 NioEventLoop 单线程池除了能够处理 本地任务Selector感兴趣的事件外,还可以处理 可调度任务

    这里的调度线程池 与 ScheduleThreadPoolExecutor 的实现有些区别, 实际上它只是 提供了PriorityQueue 优先级队列 来存放可调度任务, 并不是 DelayQueue 延迟优先级队列 。也就是说 NioEventLoop 中做了相关的阻塞处理。

    下面从提交任务的方法 execute(task) 方法来入手

    1.提交任务

        private void execute(Runnable task, boolean immediate) {
            // 判断当前提交任务的线程是否是  EventLoop线程
            boolean inEventLoop = inEventLoop();
    
            // 加入到 本地任务队列 中
            addTask(task);
    
    		// 条件成立: 当前线程不是 eventLoop线程
            if (!inEventLoop) {
                
                //开启 eventLoop线程
                startThread();
             	
                // .... 省略
            }
        }
    

    代码很简单明了:

    1. 将任务添加到本地任务队列中
    2. 判断当前线程是否是 eventLoop线程,若不是 则启动eventLoop线程

    2.启动线程

        private void startThread() {
            // 条件成立: eventLoop 状态为 非启动状态
            if (state == ST_NOT_STARTED) {
                
                // CAS 设置 eventLoop 状态为 启动状态 (线程安全考虑)
                if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                    boolean success = false;
                    try {
    					// 真正开启线程
                        doStartThread();
                        success = true;
                    } finally {
                        
                        // 若启动过程中有异常,则将状态修改为 非启动状态
                        if (!success) {
                            STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
                        }
                    }
                }
            }
        }
    

    上述代码 使用 state字段,通过CAS 来控制 eventLoop 的启动时的并发性, 而真正 启动线程的 代码就一行

    doStartThread()

        private void doStartThread() {
            assert thread == null;
            
            // 向线程执行器提交 eventLoop启动任务,真正开启线程 
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    
                    // 将该线程 保存在 eventLoop 的 thread 字段中
                    thread = Thread.currentThread();
                    if (interrupted) {
                        thread.interrupt();
                    }
    
                    boolean success = false;
                    updateLastExecutionTime();
                    try {
                        //   NioEventLoop线程 真正运行的核心逻辑
                        SingleThreadEventExecutor.this.run();
                        success = true;
                    } catch (Throwable t) {
                        logger.warn("Unexpected exception from an event executor: ", t);
                    } finally {
                    	// .... 代码省略   处理当 eventLoop 启动异常的过程
                    }   
                }
            });
        }
    

    从上述代码可知, NioEventLoop线程 实际上是通过 executor 来创建的,之后再将创建的线程 赋值给 NioEventLoop对象的 thread字段。

    而NioEventLoop线程 真正的运行核心逻辑是在 SingleThreadEventExecutor.this.run(); 方法中

    3.核心逻辑

    追踪SingleThreadEventExecutor.this.run(); 该方法,会来到 NioEventLoop #run() 方法。

        @Override
        protected void run() {
    		
            // epoll bug 特征计数变量
            // 每执行一次 select 后, 该值会 +1
            int selectCnt = 0;
    
            for (;;) {
                try {
           			// 1. >= 0时 表示 selector上的事件就绪个数
                    // 2. < 0时 表示 CONTINUE(-2)	  BUSY_WAIT(-3)  SELECT(-1) 
                    int strategy;
                    
     // ----------------1. select 获取 selector 上的就绪事件(IO事件) ---------------------     
                    try {
                        strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
    
                        switch (strategy) {
                        case SelectStrategy.CONTINUE:
                            continue;
                        case SelectStrategy.BUSY_WAIT:
      
                        case SelectStrategy.SELECT:
    
                            long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
    
                            if (curDeadlineNanos == -1L) {
    
                                curDeadlineNanos = NONE; // nothing on the calendar
                            }
    
                            nextWakeupNanos.set(curDeadlineNanos);
                            try {
    
                                if (!hasTasks()) {
    
                                    strategy = select(curDeadlineNanos);
                                }
                            } finally {
    
                                nextWakeupNanos.lazySet(AWAKE);
                            }
                            // fall through
                        default:
                        }
                    } catch (IOException e) {
                   
                        rebuildSelector0();
                        selectCnt = 0;
                        handleLoopException(e);
                        continue;
                    }
    
                    
      //-----------------2. 处理IO事件  和 本地任务 (普通任务,调度任务) --------------------------              
                    selectCnt++;
                    cancelledKeys = 0;
                    needsToSelectAgain = false;
    
                    final int ioRatio = this.ioRatio;
    
                    boolean ranTasks;
    
                    if (ioRatio == 100) {
                        try {
    
                            if (strategy > 0) {
                                processSelectedKeys();
                            }
                        } finally {
                            ranTasks = runAllTasks();
                        }
                    } else if (strategy > 0) {
                        final long ioStartTime = System.nanoTime();
                        try {
                            processSelectedKeys();
                        } finally {
                            final long ioTime = System.nanoTime() - ioStartTime;
                            ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                        }
                    } else {
                        ranTasks = runAllTasks(0); // This will run the minimum number of tasks
                    }
    
    
                    if (ranTasks || strategy > 0) {
                        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                            logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                    selectCnt - 1, selector);
                        }
                        selectCnt = 0;
                    } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                        selectCnt = 0;
                    }
                } 
                
                
                
              // .... 下面 不是核心逻辑了 ..........  
                catch (CancelledKeyException e) {
                    // Harmless exception - log anyway
                    if (logger.isDebugEnabled()) {
                        logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                                selector, e);
                    }
                } catch (Error e) {
                    throw (Error) e;
                } catch (Throwable t) {
                    handleLoopException(t);
                } finally {
                    // Always handle shutdown even if the loop processing threw an exception.
                    try {
                        if (isShuttingDown()) {
                            closeAll();
                            if (confirmShutdown()) {
                                return;
                            }
                        }
                    } catch (Error e) {
                        throw (Error) e;
                    } catch (Throwable t) {
                        handleLoopException(t);
                    }
                }
            }
        }
    

    上述代码很长, 我们主要分两部分来看, 我分别用 分割线(-----------------------) 在代码中隔开了, 从分割线上的注释能知道,这一大段代码主要的 功能如下:

    1.  select 获取 selector 上的就绪事件(IO事件)
    2.  处理IO事件  和 本地任务 (普通任务,调度任务)
    

    3.1 部分一

    我们首先分析第一部分的代码

                    try {
                        
                        // 根据当前NioEventLoop 是否有本地普通任务,来决定怎么处理
                        // 1. 有任务,调用多路复用器 selectNow() 方法 返回 多路复用器上 就绪ch个数
                        // 2. 没有任务 返回 -1(SELECT)
                        strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
    
                        switch (strategy) {
                        case SelectStrategy.CONTINUE:
                            continue;
                        case SelectStrategy.BUSY_WAIT:
      					
                        // 当没有 本地普通任务 时,会来到该分支 ...
                        case SelectStrategy.SELECT:
    						
                            // 计算 最近的 本地可调度任务 的截止时间    
                            long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
    						
                            // 条件成立:没有 本地可调度任务,则 curDeadlineNanos = Long.MAX    
                            if (curDeadlineNanos == -1L) {
                                curDeadlineNanos = NONE; // nothing on the calendar
                            }
                            nextWakeupNanos.set(curDeadlineNanos);
                                
                            try {
    							// 条件成立: 没有 本地普通任务
                                if (!hasTasks()) {
    								// 则阻塞 select(time) 
                                    strategy = select(curDeadlineNanos);
                                }
                            } finally {
                                nextWakeupNanos.lazySet(AWAKE);
                            }
                            // fall through
                         // 当有本地任务时, 则会直接走到第二部分       
                        default:
                        }
                    } catch (IOException e) {
                   
                        rebuildSelector0();
                        selectCnt = 0;
                        handleLoopException(e);
                        continue;
                    }
    

    上述代码 主要分为两种情况,(其中复杂点的为情况2):

    1. 本地普通任务 : 立即调用 selectNow() 获取就绪事件个数
    2. 没有 本地普通任务
      1. 获取 最近 本地调度任务 的执行时间点
      2. 本地调度任务 , 则定时阻塞select( time)本地调度任务 该执行的时间点
      3. 没有 本地调度任务 ,则一直阻塞 select() ,直到有 就绪事件到来为止.

    3.2 部分二

                 
    				// 到此 上面是 做过select操作了,因此 selectCnt+1
                    selectCnt++;
                    cancelledKeys = 0;
                    needsToSelectAgain = false;
    				
    				// 表示下面 处理 IO事件的 时间占比
                    final int ioRatio = this.ioRatio;
    
                    boolean ranTasks;
    				
    				// 条件成立: 处理IO事件(就绪事件) 时间占比为 100%
                    if (ioRatio == 100) {
                        try {
    						// 条件成立: 有IO事件(就绪事件)
                            if (strategy > 0) {
                                // 处理IO事件(就绪事件) 的核心方法
                                processSelectedKeys();
                            }
                        } finally {
                            // 处理 本地任务 的核心方法
                            ranTasks = runAllTasks();
                        }
                        
                    // 条件成立: IO时间占比不是100%  且 有IO事件(就绪事件)    
                    } else if (strategy > 0) {
        				//  获取系统当前时间
                        final long ioStartTime = System.nanoTime();
                        try {
                            // 处理IO事件(就绪事件)
                            processSelectedKeys();
                        } finally {
                            // 计算 执行IO事件(就绪事件) 耗费了 多长时间
                            final long ioTime = System.nanoTime() - ioStartTime;
                            // 根据IO的执行时间,计算出本地任务需要执行多长时间
                            // 按照规定时间,执行本地任务
                            ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                        }
                        
                    // 条件成立: IO时间占比不是100%  且 没有IO事件(就绪事件)
                    } else {
                        // 处理最多64个本地任务
                        ranTasks = runAllTasks(0); 
                    }
    
    				
    				// 条件成立: 本地任务执行成功 或者 IO就绪事件个数>0
                    if (ranTasks || strategy > 0) {
                        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                            logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                    selectCnt - 1, selector);
                        }
                        // 将 selectCnt 置为0
                        selectCnt = 0;
                    }
    
    				// 来到这说明: 本次轮询 既没有 执行本地任务   也没有 IO就绪事件
    				
    				// 处理 epoll Bug
    				else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                        selectCnt = 0;
                    }
                } 
    

    部分二中的代码, 实际上也很好理解, 我们首先要清楚 NioEventLoop 线程主要处理 的两件事 :

    1.  **IO 事件 (selector的就绪事件)**                                     **processSelectedKeys()**        
    2.  **本地任务 (分为 本地普通任务 和  本地调度任务) **       **runAllTasks()**
    

    另外需要 注意 变量 ioRatio: 该变量主要表示 执行IO事件 在总处理时间(IO事件 + 本地任务)上的占比。

    processSelectedKeys()runAllTasks() 的代码分析,下面会做详细分析。

    注意处理本地任务的方法 在上述代码中有三种:

    1. runAllTasks();
    2. runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    3. runAllTasks(0); 
    

    4. 处理IO事件

        private void  processSelectedKeysOptimized() {
            // 遍历就绪事件集合
            for (int i = 0; i < selectedKeys.size; ++i) {
    
                // SelectionKey 表示就绪事件
                final SelectionKey k = selectedKeys.keys[i];
    
                selectedKeys.keys[i] = null;
    
                // 拿到就绪事件上的附件  这里会拿到注册阶段,咱们向Selector提供的Channel对象
                final Object a = k.attachment();
    
                if (a instanceof AbstractNioChannel) {
    
                    // 处理IO事件...
                    processSelectedKey(k, (AbstractNioChannel) a);
                } else {
                    @SuppressWarnings("unchecked")
                    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                    processSelectedKey(k, task);
                }
    
                if (needsToSelectAgain) {
      
                    selectedKeys.reset(i + 1);
    
                    selectAgain();
                    i = -1;
                }
            }
        }
    

    总结上述代码中的 操作如下:

    1. 遍历 **selectedKeys**  事件就绪集合 ,拿到一个个**SelectionKey**
    2. 通过获取 **SelectionKey** 中的 **attchment** 可以拿到 与该就绪事件绑定的 **Channel**
    3. 调用  `processSelectedKey(k, (AbstractNioChannel) a);` 按照不同的事件做对应的处理
    

    针对不同的就绪事件,做不同的处理操作:

        private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    
            // 1. NioServerSocketChannel -> NioMessageUnsafe
            // 2. NioSocketChannel -> NioByteUnsafe
            final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
            if (!k.isValid()) {
                final EventLoop eventLoop;
                try {
                    eventLoop = ch.eventLoop();
                } catch (Throwable ignored) {
                    return;
                }
                if (eventLoop == this) {
                    unsafe.close(unsafe.voidPromise());
                }
                return;
            }
    
            try {
                // 获取就绪事件
                int readyOps = k.readyOps();
                
                // 1.就绪事件为: OP_CONNECT
                if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                    int ops = k.interestOps();
                    ops &= ~SelectionKey.OP_CONNECT;
                    k.interestOps(ops);
    
                    unsafe.finishConnect();
                }
                
                // 2.就绪事件为:OP_WRITE
                if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                    ch.unsafe().forceFlush();
                }
    
              	
                // 3.就绪事件为:OP_READ 或者 OP_ACCEPT
                if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                   // 这里针对 客户端 或者 服务端的Channel 做不同的 read操作
                   // 1. 服务端Channel accept 事件
                   // 2. 客户端Channel read 事件
                    unsafe.read();
                }
            } catch (CancelledKeyException ignored) {
                unsafe.close(unsafe.voidPromise());
            }
        }
    

    上述代码很简单,实际上类似于 我们最基本的 NIO 编程案例, 根据不同的 就绪事件 做不同的操作。

    最终处理的方法将会根据 Channel 的不同,调用其内部类 unsafe中的方法来处理。

    具体不同的 Channel 中 unsafe 处理的细节源码 会在下一篇文章《服务端/客户端 Channel消息处理源码分析》中做详细介绍。

    5. 处理本地任务

    在核心逻辑的第二部分中,强调了处理本地任务的方法 有三种:

    1. **runAllTasks();**
    2. **runAllTasks(ioTime * (100 - ioRatio) / ioRatio);**
    3. **runAllTasks(0);** 
    

    实际上 是两个 重载方法, 第2 和 第3个是属于同一种方法

    1. runAllTasks() 空参的
    2. runAllTasks(long timeoutNanos) 带有时间参数的

    首先来看 空参的runAllTasks()

        // 返回: 是否执行了任务 
    	protected boolean runAllTasks() {
            assert inEventLoop();
            boolean fetchedAll;
            boolean ranAtLeastOne = false;
    	
            do {
                // fetchFromScheduledTaskQueue() 将 调度任务队列中 需要被调度的任务 转移到普通任务队列taskQueue内
                // fetchedAll表示 需要被调度的任务 有没有 转移完
                fetchedAll = fetchFromScheduledTaskQueue();
    
    			// 条件成立: 执行了任务
                if (runAllTasksFrom(taskQueue)) {
                    ranAtLeastOne = true;
                }
            } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
    
            // 执行到这 需要调度的任务和普通任务全部都执行完了
    
            if (ranAtLeastOne) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
            }
            afterRunningAllTasks();
            return ranAtLeastOne;
        }
    

    上面代码 没什么好说的,主要做的目的就是, 将 本地调度任务队列中的可调度任务 转移到 本地普通任务队列中 去执行。

    再来看 有参的 runAllTasks(long timeoutNanos)

        // 参数: 表示执行任务最多可用的时长
        protected boolean runAllTasks(long timeoutNanos) {
    
            // 转移 需要被调度任务到 普通任务队列
            fetchFromScheduledTaskQueue();
            Runnable task = pollTask();
            if (task == null) {
                afterRunningAllTasks();
                return false;
            }
    
            // deadline 表示执行任务的截止时间
            final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
    
            // 表示已经执行的任务个数
            long runTasks = 0;
    
            // 最后一个任务的执行时间戳
            long lastExecutionTime;
    
    
            for (;;) {
                // 执行任务
                safeExecute(task);
    
                runTasks ++;
    
    
                // 0x3f=> 十进制63  二进制111111
                //64 => 1000000 & 000000 = 0
                //128 => 10000000 & 000000 = 0
                //192 => 11000000 & 000000 = 0
                // 结论: 每执行64个任务  这个条件会成立一次,
                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;
        }
    

    runAllTasks(long timeoutNanos)方法与 上面空参的 runAllTasks() 不同的点如下:

    1.  **将可调度任务 转移到 本地普通任务队列中的 操作 只做一次** 
    2.   **if ((runTasks & 0x3F) == 0)**   每执行64个任务,该条件就会成立一次, 来判断 执行任务的时间是否超过了**timeoutNanos 超时时间**
      1. 超过了  则立即退出
      2. 没超过  则继续执行任务,直到再次执行了 64个任务,又会进入该判断。
    

    6.总结

    至此 NioEventLoop 运行相关的代码分析完毕, 本篇文章需要弄清楚以下几点:

    1. NioEventLoop 线程 需要处理的 事情有:
    2. IO事件 (selector就绪事件)
    3. 本地任务 (普通任务, 调度任务)
    4. 处理IO事件 总是优先于 本地任务, 具体的执行时间配比 按照 ioRatio 字段来决定。
    5. 处理IO事件时,会根据 就绪事件的不同 和 Channel 的不同 来做具体的操作。
    6. 处理本地任务时
    7. 会先将 可调度任务转移到本地普通任务队列中
    8. 逐个处理 本地普通任务队列 (包括 普通任务 和 可调度任务) 中的任务.

    具体 服务端 / 客户端 Channel 如何 处理 IO事件 的源码,会在下一篇文章中详细分析。

  • 相关阅读:
    ABAP 没有地方输入H 进入DEBUG 怎么办?
    Jsoup实现java模拟登陆
    Jsoup模拟登陆例子
    Jsoup:解决java.net.UnknownHostException的问题
    Java抓取网页数据(原网页+Javascript返回数据)
    利用StringEscapeUtils对字符串进行各种转义与反转义(Java)
    MyEclipse + Tomcat 热部署问题
    管道寄售库存MRKO结算后,冲销问题
    c#操作appsettiongs
    让你的微信小程序具有在线支付功能
  • 原文地址:https://www.cnblogs.com/s686zhou/p/15896265.html
Copyright © 2020-2023  润新知