【Netty4】Netty核心组件之NioEventLoop(一)创建

系类文章:
《Netty服务端启动源码分析(一)整体流程》
《Netty服务端启动源码分析(二)服务端Channel的端口绑定》
《Netty核心组件之NioEventLoop(一)创建》
《Netty核心组件之NioEventLoop(二)处理消息》

1. 开篇

  • NioEventLoop是Netty框架的Reactor线程;

  • NioEventLoop负责处理注册在其上面的所有Channel的IO事件,通常情况下一个NioEventLoop会下挂多个Channel,但一个Channel只和唯一的NioEventLoop对应

    例如NioEventLoop A B 2个,A和c1、c2对应,B和c3、c4对应,不会交叉。
    一个NioEventLoop对应一个selector多路复用器,为了负载均衡,新的Channel会平摊到每个NioEventLoop上,一旦确定后,就不会更改。

  • NioEventLoop同时会负责通过execute方法提交的任务,以及通过schedule方法提交的定时任务;

在接下来几篇文章,我会通过Netty的源码深入讲解NioEventLoop的实现机制。

特别说明:基于4.1.52版本的源码(很新的版本,和之前4.xx版本还是有些不同的)

2. 类继承关系以及重要的成员变量

先来看下NioEventLoop的类关系图和重要的属性,对其有一个整体的感知,便于后面详细分析。

2.1 类继承关系

在这里插入图片描述
可以看到NioEventLoop的继承关系非常复杂,最上层是JDK的Executor接口,说明它归根到底是一个执行器,是用来执行任务的。另外,它实现了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,继承了SingleThreadEventExecutor类,这些接口和类为这个执行器添加了十分繁复的功能特性,要搞清楚NioEventLoop的具体实现机制就要不停的在这些父类和接口中来回跳转。

  • ScheduledExecutorService接口表示是一个定时任务接口,EventLoop可以接受定时任务。

  • EventLoop接口: 一旦Channel注册了,就处理该Channel对应的所有I/O操作。

  • SingleThreadEventExecutor表示这是一个单个线程的线程池。

  • EventLoop是一个单例的线程池,里面含有一个死循环的线程不断地做着三件事情:监听端口,处理端口事件,处理队列事件。每个Eventloop都可以绑定多个Channel,而每个Channel始终只能由一个EventLoop来处理

    也就是说,创建了NioEventLoop后,拥有一个线程池类型的成员变量,紧接着会往该线程池加入一个线程,该线程就负责阻塞监听IO事件等操作。添加线程由SingleThreadEventExecutor的doStartThread()完成,线程具体的干的事情由NioEventLoop的run()实现

2.2 重要的成员变量

private Selector selector;
private SelectedSelectionKeySet selectedKeys;
private volatile Thread thread;
private final EventExecutorGroup parent;
private final Queue<Runnable> taskQueue;
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
private final Queue<Runnable> tailTasks;
  • selector:作为NIO框架的Reactor线程,NioEventLoop需要处理网络IO事件,因此它需要有一个多路复用器,即Java NIO的Selector对象;
  • selectedKeys:每次select操作选出来的有事件就绪的SelectionKey集合,在NioEventLoop的run方法中会处理这些事件;
  • thread即每个NioEventLoop绑定的线程,它们是一对一的关系,一旦绑定,在整个生命周期内都不会改变;
  • parent:即当前的NioEventLoop所属的EventExecutorGroup;
  • taskQueue:NioEventLoop中三大队列之一,用于保存需要被执行的任务。
  • scheduledTaskQueue:NioEventLoop中三大队列之一,是一个优先级队列(内部其实是一个按照任务的下次执行时间排序的小顶堆),用于保存定时任务,当检测到定时任务需要被执行时,会将任务从scheduledTaskQueue中取出,放入taskQueue;
  • tailTasks:NioEventLoop中三大队列之一,用于存储当前或下一次事件循环结束后需要执行的任务;

2.3 构造函数

在调用构造函数创建实例之前,有必要回顾下NioEventLoop是何时被创建的,是在创建NioEventLoopGroup的时候就已经创建了!详情参见《Netty服务端启动源码分析(一)》 中"1.1.2 构造函数干了什么"章节。也就是说NioEventLoop是被预创建的,发生在通道的创建和与通道的绑定之前。

首先来看NioEventLoop的构造函数:

    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
                 EventLoopTaskQueueFactory queueFactory) {
        // 设置parent、executor、addTaskWakesUp(添加任务时是否唤醒select)、创建taskQueue和tailTask队 
        // 列
        super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
                rejectedExecutionHandler);
        this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
        this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
        // selector初始化
        final SelectorTuple selectorTuple = openSelector();
        this.selector = selectorTuple.selector;
        this.unwrappedSelector = selectorTuple.unwrappedSelector;
    }

2.3.1 在构造函数中,会创建任务队列和tailTask队列

    private static Queue<Runnable> newTaskQueue(
            EventLoopTaskQueueFactory queueFactory) {
        if (queueFactory == null) {
            return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
        }
        return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
    }

    private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
        return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
                : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
    }

默认情况下,会创建MPSC,即多生产者单消费者的队列,这里最终会用到JCTools库,这里不过多介绍,感兴趣的可以自己去了解。

2.3.2 多路复用器Selector

Netty的实现是基于Java原生的NIO的,其对原生的NIO做了很多优化,避免了某些bug,也提升了很多性能。但是底层对于网络IO事件的监听和处理也是离不开多路复用器Selector的。在NioEventLoop的构造方法中进行了Selector的初始化:

final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;

构造函数中还会初始化selector和根据配置对selectedKeys进行优化

    private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        try {
            unwrappedSelector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }
        // 如果优化选项没有开启,则直接返回
        if (DISABLE_KEY_SET_OPTIMIZATION) {
            return new SelectorTuple(unwrappedSelector);
        }

        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
                    Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }

                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                    return e;
                } catch (IllegalAccessException e) {
                    return e;
                }
            }
        });
        selectedKeys = selectedKeySet;
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }
  • 调用openSelector();,初始化一个epoll,并持有这个selector
    等价于 原生nio语法:Selector selector = Selector.open();

    更多的nio知识,可以参见:《bio、nio、aio–Netty笔记》中<3.2 NIO引入多路复用器代码示例>的架构示意图部分

如果设置了优化开关(默认优化选项是开启的),则通过反射的方式从Selector中获取selectedKeys和publicSelectedKeys,将这两个成员设置为可写,通过反射,使用Netty构造的selectedKeySet将原生JDK的selectedKeys替换掉。

我们知道使用Java原生NIO接口时,需要先调Selector的select方法,再调selectedKeys方法才可以获得有IO事件准备好的SelectionKey集合。这里优化过后,只通过一步select调用,就可以从selectedKeySet获得需要的SelectionKey集合。

另外,原生Java的SelectionKey集合是一个HashSet,这里优化过后的SelectedSelectionKeySet底层是一个数组,效率更高。

2.4 NioEventLoop执行流程

也就是说,创建了NioEventLoop后,拥有一个线程池类型的成员变量,紧接着会往该线程池加入一个线程,该线程就负责阻塞监听IO事件等操作。添加线程由SingleThreadEventExecutor的doStartThread()完成,线程具体的干的事情由NioEventLoop的run()实现。

2.4.1 添加一个线程

我们先来看下添加线程的代码,定义在父类中的,为了方便,我进行了简化,仅列出重要代码:

public abstract class SingleThreadEventExecutor {
    private void doStartThread() {
        
        //添加一个Runnable作为线程任务到executor(线程池)中
        executor.execute(new Runnable() {
            @Override
            public void run() {               
             
                try {
                    //Runnable匿名内部类调用外部类SingleThreadEventExecutor的run(),实际会调用子类NioEventLoop对象实例的run()
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                   
                }
            }
        });
    }

代码比较简单,但是有个不容易理解的地方,即SingleThreadEventExecutor.this.run();,这个是匿名表达式引用外部类方法的语法,即Runnable的run()方法想引用外部类的run()方法,详情参见《Lambda表达式里的“陷阱“ 匿名表达式 qualified this》

2.4.2 run方法解析

添加线程后,线程池就会调用该线程,那么线程具体干了哪些事情呢?

EventLoop的职责可以用下面这张图形象的表示:
在这里插入图片描述
EventLoop的run方法在一个for死循环中,周而复始的做着三件事:

1、从已注册的Channel监听IO事件;对应select()阻塞监听,当然为了避免一直阻塞,一般带超时时间
2、处理IO事件;
3、从任务队列取任务执行。

public final class NioEventLoop extends SingleThreadEventLoop {
    protected void run() {
        int selectCnt = 0;
        for (;;) {
              int strategy;
              try {
                    // 计算本次循环的执行策略
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.BUSY_WAIT:
                    case SelectStrategy.SELECT:
                      // 调用Java NIO的多路复用器,检查注册在NioEventLoop上的Channel的IO状态
                      strategy = select(curDeadlineNanos);
                    }
                } catch (IOException e) {
                }
                // 处理IO事件
                processSelectedKeys();
                // 处理任务队列中的任务
                ranTasks = runAllTasks();
                ...
    }

整个run()方法被包裹在一个for循环中,唯一能够结束循环的条件是状态state为SHUTDOWN或者TERMINATED,NioEventLoop继承了SingleThreadEventExecutor,isShuttingDown()和confirmShutdown()都是SingleThreadEventExecutor中的方法。

可以看到,除去异常处理和一些分支流程,整个run()方法不是特别复杂,重点在与select()和selectNow()方法,run()方法流程如下图所示:

在这里插入图片描述

下面详细解析

2.4.2.1 calculateStrategy

calculateStrategy决定了调用selectNow()还是select()。

先来看calculateStrategy:

    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }

   //成员变量selectNowSupplier 
    private final IntSupplier selectNowSupplier = new IntSupplier() {
        @Override
        public int get() throws Exception {
            return selectNow();
        }
    };

    protected boolean hasTasks() {
        assert inEventLoop();
        return !taskQueue.isEmpty() || !tailTasks.isEmpty();
    }
  • 每次循环,都会检测任务队列和IO事件,如果任务队列中没有任务,则直接返回SelectStrategy.SELECT;

    因为任务列表为空,则允许调用select阻塞等待新的消息的到来

  • 如果任务队列中有任务,则会调用非阻塞的selectNow检测是否有IO事件准备好的Channel数。

    因为任务列表不为空,要选用非阻塞的方法,这样原有的任务可以继续执行

2.4.2. 阻塞的select

当任务队列中没有任务时,直接进入select分支

       case SelectStrategy.SELECT:
            // 找到下一个将要执行的定时任务的截止时间
            long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
            if (curDeadlineNanos == -1L) {
                curDeadlineNanos = NONE; // nothing on the calendar
            }
            nextWakeupNanos.set(curDeadlineNanos);
            try {
                if (!hasTasks()) {
                    // 阻塞调用select
                    strategy = select(curDeadlineNanos);
                }
            } finally {
                nextWakeupNanos.lazySet(AWAKE);
            }
            // fall through

阻塞调用select:

    private int select(long deadlineNanos) throws IOException {
        // 如果没有定时任务,直接调Java NIO的select,进入阻塞
        if (deadlineNanos == NONE) {
            return selector.select();
        }
        // 如果截止时间小于0.5ms,则timeoutMillis 为0,直接调非阻塞的selectNow()方法
        long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
        return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
    }

在调用select之前,再次调用hasTasks()判断从上次调用该方法到目前为止是否有任务加入,多做了一层防护,因为调用select时,可能会阻塞,这时,如果任务队列中有任务就会长时间得不到执行,所以须小心谨慎。

如果任务队列中还是没有任务,则会调用select方法。在这个方法中会根据入参deadlineNanos来选择调用NIO的哪个select方法:

如果deadlineNanos为NONE,即没有定时任务时,直接调用NIO的无参select方法,进入永久阻塞,除非检测到Channel的IO事件或者被wakeup;
如果存在定时任务,且定时任务的截止时间小于0.5ms,则timeoutMillis 为0,直接调非阻塞的selectNow方法,也就是说马上有定时任务需要执行了,不要再进入阻塞了;
其他情况,调用select(timeout),进入有超时时间的阻塞。

2.4.2.3 processSelectedKeys()

参见 《Netty核心组件之NioEventLoop(二)处理消息》

总结

在本文中,对Netty的NioEventLoop进行了深入的解读,并且详细讲解了它的三大职责之一:检测Channel的IO事件的机制。

NioEventLoop是Netty最核心的概念,内部运行机制很复杂,在接下来的两篇文章中会继续分析。

参考:
《Netty核心组件之NioEventLoop(一)》

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值