Netty源码学习1——NioEventLoopGroup的初始化

发布时间 2023-08-13 17:55:03作者: Cuzzz

系列文章目录和关于我

零丶引入

netty源码学习中,大家maybe都接触到如下的hello world——netty客户端启动的demo:

image-20230813113511153
映入眼帘的第一个类就是NioEventLoopGroup,很多文章上来就是是Netty中的核心类,啥Channel,Pipeline,Context,Boostrap一通劈里啪啦,我看起来比较费劲。

so本文不会上来就给大家介绍netty中所有的组件,而是先从NioEventLoopGroup入手。

一丶何为NioEventLoopGroup & NioEventLoopGroup继承关系

image-20230813114035210

上图为NioEventLoopGroup继承关系,根据源码上的注释我们可以大概了解这些类的作用:

  • EventExecutorGroup:通过next方法提供EventExecutor,并且还负责处理它们的生命周期,并允许以全局方式关闭它们(指shutdownGracefully关闭EventExecutorGroup中的所有EventExecutor)

    image-20230813114502850

  • EventExecutor:EventExecutor 是一个特殊的 EventExecutorGroup(next方法指只会返回自己),它附带了一些方便的方法来查看线程是否在事件循环中执行。

    image-20230813114734411

  • EventLoopGroup:特殊EventExecutorGroup,允许注册在事件循环期间注册Channel

    Channel是一个连接网络输入和IO处理的桥梁。可以通过Channel来判断当前的状态,是open还是connected,还可以判断当前Channel支持的IO操作。
    

    image-20230813115024609

  • AbstractEventExecutorGroup:EventExecutorGroup的抽象实现,可以看到对AbstractEventExecutorGroup提交任务,最终都会被其使用next获取EventExecutor进行处理。

    EventExecutor是打工仔,AbstractEventExecutorGroup是分配任务的leader

    image-20230813115730045

  • MultithreadEventExecutorGroup:从名字上可以看出这是一个多线程的EventExecutorGroup,其中的“线程” = EventExecutor

    image-20230813120101605

  • MultithreadEventLoopGroup:多线程的EventExecutorGroup + EventLoopGroup = 多线程处理任务且允许Channel注册的EventLoopGroup,下面猫一眼其Channel的注册:

    image-20230813120527154

    可以看到还是交给了打工人EventLoop

  • NioEventLoopGroup:MultithreadEventLoopGroup实现,其关联的Channel是基于NIO Selector的Channel实现的

二丶NioEventLoopGroup的初始化

image-20230813145926579 image-20230813145439465

在Netty 入门Demo中无论是Server还是Client都会先初始化NioEventLoopGroup,下面对于这个初始化过程进行源码解析。

image-20230813150118112

  • 参数中的SelectorProvider 是由SelectorProvider.provider() 提供的,SelectorProvider #openSelector可以创建selector,不同的操作系统这里会拿到不同的SelectorProvider

  • DefaultSelectStrategyFactory.INSTANCE是SelectStrategyFactory的默认实现,其newSelectStrategy会提供DefaultSelectStrategy作为选择策略,这个策略在Netty NioEventLoop#run方法来左右程序的执行(这点后续详细分析)

    image-20230813150523160

最终NioEventLoopGroup的构造方法将调用父类MultithreadEventExecutorGroup的构造方法

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                        EventExecutorChooserFactory chooserFactory, Object... args) {
    checkPositive(nThreads, "nThreads");

    if (executor == null) {
        // 初始化executor
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }
 
    children = new EventExecutor[nThreads];

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            // 创建EventLoop
            children[i] = newChild(executor, args);
            success = true;
        } catch (Exception e) {
            throw new IllegalStateException("failed to create a child event loop", e);
        } finally {
            // s省略关闭children数组中EventLoop的代码
        }
    }
  
    // 选择器,EventLoopGroup#next方法依赖此选择器选择EventLoop
    chooser = chooserFactory.newChooser(children);
    
    // 此处省略 EventLoopGroup关闭的Future回调
    
    // 只读set
    Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
    Collections.addAll(childrenSet, children);
    readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

其中比较有意思的是newChild方法,此方法由NioEventLoopGroup进行实现

image-20230813153046662

且NioEventLoopGroup中Executor将作为参数进行使用,目前我尚不知这个Executor的作用,但是可以先看一下ThreadPerTaskExecutor特性

image-20230813153322350

DefaultThreadFactory#newThread如下

image-20230813153452941

可以看到每一个Thread都是FastThreadLocalThread,每一个任务都会包装为FastThreadLocalRunnable#wrap方法包装,以保证FastThreadLocal会在任务执行后进行释放(和TransmittableThreadLocal的做法类似,都是对原有Thread,和任务的包装)

image-20230813153551124

Netty的FastThreadLocal的奥妙后续会单独进行学习和分析。

三丶NioEventLoop

image-20230813152953158

在学习NioEventLoopGroup是如何创建NioEventLoop之前,我们先看下NioEventLoop的继承体现。

  • SingleThreadEventExecutor:单线程Exectuor,内部持有一个Thread,在第一次提交任务的时候会将任务放到任务队列,并启动内部的Thread,该Thread执行逻辑交由子类实现
  • EventLoop:Channel注册到EventLoop中,后续由EventLoop处理这些Channel
  • SingleThreadEventLoop:SingleThreadEventExecutor + EventLoop = 单线程处理任务的EventLoop

四丶NioEventLoopGroup#newChild创建NioEventLoop

image-20230813154819601

值得一说是还是这个executor参数,在NioEventLoop调用父类构造函数的时候,这个executor 会使用ThreadExecutorMap.apply包装一下,会将当前的NioEventLoop记录到FastThreadLocal中,让任务的执行过程可以从FastThreadLocal中拿到当前NioEventLoop

image-20230813155111719

1.Netty对于Selector的优化

image-20230813155853959

在NioEventLoop的创建时,会执行openSelector返回一对Selector,其中一个时原生的JDK中的Selector,另外一个时Netty优化的Selector,我们看下Netty做了什么优化

image-20230813160242297

可以看到Netty会反射修该原JDK中的Selector 的selectedKeys和publicSelectedKeys字段,在原生JDK Selector中这两个Set的作用:

  • selectedKeys:Selector会将自己监听到的IO就绪Channel放到selectedKeys

  • publicSelectedKeys:selectedKeys 的视图,用于向外部线程返回IO就绪SelectionKey。这个集合在外部线程中只能做删除操作不可增加元素,并且`不是线程安

    全的

Netty为啥要进行优化昵

  • SelectorImpl监听到IO就绪SelectionKey 后,会将就绪IO对应的`SelectionKey add到selectedKeys集合中

  • IO就绪后会唤醒阻塞在SelectorImpl#select方法上的线程,这些线程将调用SelectorImpl#selectedKeys,进行遍历处理,这里将返回publicSelectedKeys

    image-20230813161929646

这里的add和遍历操作都是针对set(HashSet)的,HashSet底层基于HashMap存在Hash冲突导致其插入需要使用拉链法,遍历的时候也无法使用 CPU 缓存的优势来提高遍历的效率(指数组元素都在紧密排布大概率在一个缓存行,可以利用预读优势)因此Netty将其优化为自己实现的SelectedSelectionKeySet

image-20230813162459675

最终会将JDK元素的Selector包装为SelectedSelectionKeySetSelector

image-20230813162653039

可以看到每次执行select方法会对selectedKeys进行处理,其实就是清除原生JDK Selector中的selectedKeys字段和publicSelectedKeys中的SelectionKey,为什么要这么做昵,让我们回顾原生JDK Selector的使用

image-20230813162927085

在selector#selectedKeys读取到publicSelectedKeys中就绪的就绪的IO实现后,会进行处理,处理完成后会进行clear方法清除已经处理的SelectionKey,但是Netty优化后的SelectedSelectionKeySet是不支持remove的,而是在SelectedSelectionKeySetSelector下一次select的时候进行批量清除。

image-20230813163217029

2.NioEventLoop任务队列的创建Mpsc队列

image-20230813163840474 image-20230813164108541

最终创建的MpscUnboundedArrayQueue或者MpscUnboundedAtomicArrayQueue,Mpsc是Multiple producers and single consumers 多生产者单消费者的缩写,它支持多个生产者多个消费者的情况并且性能强劲(cas无锁设计减少了频繁的唤醒和阻塞,内部使用数组在需要扩容的时候不是复制旧数组,而是数组尾部元素指向下一个数组)

3.NioEventLoop中的任务队列

image-20230813172617175

事件循环中存在的事件队列,在NioEventLoop中的体现就是三个任务队列

  • taskQueue:就是上面提到的Mpsc队列,NioEventLoop中的线程run会处理其中的任务
  • tailTaskQueue:尾部队列,处理完taskQueue中所有任务后,会获取tailTaskQueue中的任务进行执行
  • scheduledTaskQueue:PriorityQueue<ScheduledFutureTask<?>>默认是基于数组的堆,用来存在延时任务

五丶总结

学习了NioEventLoopGroup 和 NioEventLoop的初始化,后续NioEventLoop 事件循环中的循环进行源码分析。