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事件 的源码,会在下一篇文章中详细分析。

posted @ 2022-02-15 14:13  s686编程传  阅读(83)  评论(0编辑  收藏  举报