Netty 源码分析之 三 我就是大名鼎鼎的 EventLoop(一)

目录

源码之下无秘密 ── 作最好的 Netty 源码分析教程git

此文章已同步发送到个人 github

简述

这一章是 Netty 源码分析 的第三章, 我将在这一章中你们一块儿探究一下 Netty 的 EventLoop 的底层原理, 让你们对 Netty 的线程模型有更加深刻的了解.

NioEventLoopGroup

Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (客户端) 章节中咱们已经知道了, 一个 Netty 程序启动时, 至少要指定一个 EventLoopGroup(若是使用到的是 NIO, 那么一般是 NioEventLoopGroup), 那么这个 NioEventLoopGroup 在 Netty 中到底扮演着什么角色呢? 咱们知道, Netty 是 Reactor 模型的一个实现, 那么首先从 Reactor 的线程模型开始吧.

关于 Reactor 的线程模型

首先咱们来看一下 Reactor 的线程模型.
Reactor 的线程模型有三种:

  • 单线程模型

  • 多线程模型

  • 主从多线程模型

首先来看一下 单线程模型:

clipboard.png

所谓单线程, 即 acceptor 处理和 handler 处理都在一个线程中处理. 这个模型的坏处显而易见: 当其中某个 handler 阻塞时, 会致使其余全部的 client 的 handler 都得不到执行, 而且更严重的是, handler 的阻塞也会致使整个服务不能接收新的 client 请求(由于 acceptor 也被阻塞了). 由于有这么多的缺陷, 所以单线程Reactor 模型用的比较少.

那么什么是 多线程模型 呢? Reactor 的多线程模型与单线程模型的区别就是 acceptor 是一个单独的线程处理, 而且有一组特定的 NIO 线程来负责各个客户端链接的 IO 操做. Reactor 多线程模型以下:

clipboard.png

Reactor 多线程模型 有以下特色:

  • 有专门一个线程, 即 Acceptor 线程用于监听客户端的TCP链接请求.

  • 客户端链接的 IO 操做都是由一个特定的 NIO 线程池负责. 每一个客户端链接都与一个特定的 NIO 线程绑定, 所以在这个客户端链接中的全部 IO 操做都是在同一个线程中完成的.

  • 客户端链接有不少, 可是 NIO 线程数是比较少的, 所以一个 NIO 线程能够同时绑定到多个客户端链接中.

接下来咱们再来看一下 Reactor 的主从多线程模型.
通常状况下, Reactor 的多线程模式已经能够很好的工做了, 可是咱们考虑一下以下状况: 若是咱们的服务器须要同时处理大量的客户端链接请求或咱们须要在客户端链接时, 进行一些权限的检查, 那么单线程的 Acceptor 颇有可能就处理不过来, 形成了大量的客户端不能链接到服务器.
Reactor 的主从多线程模型就是在这样的状况下提出来的, 它的特色是: 服务器端接收客户端的链接请求再也不是一个线程, 而是由一个独立的线程池组成. 它的线程模型以下:

clipboard.png

能够看到, Reactor 的主从多线程模型和 Reactor 多线程模型很相似, 只不过 Reactor 的主从多线程模型的 acceptor 使用了线程池来处理大量的客户端请求.

NioEventLoopGroup 与 Reactor 线程模型的对应

咱们介绍了三种 Reactor 的线程模型, 那么它们和 NioEventLoopGroup 又有什么关系呢? 其实, 不一样的设置 NioEventLoopGroup 的方式就对应了不一样的 Reactor 的线程模型.

单线程模型

来看一下下面的例子:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
 .channel(NioServerSocketChannel.class)
 ...

注意, 咱们实例化了一个 NioEventLoopGroup, 构造器参数是1, 表示 NioEventLoopGroup 的线程池大小是1. 而后接着咱们调用 b.group(bossGroup) 设置了服务器端的 EventLoopGroup. 有些朋友可能会有疑惑: 我记得在启动服务器端的 Netty 程序时, 是须要设置 bossGroup 和 workerGroup 的, 为何这里就只有一个 bossGroup?
其实很简单, ServerBootstrap 重写了 group 方法:

@Override
public ServerBootstrap group(EventLoopGroup group) {
    return group(group, group);
}

所以当传入一个 group 时, 那么 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup 了.
这时候呢, 由于 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup, 而且这个 NioEventLoopGroup 只有一个线程, 这样就会致使 Netty 中的 acceptor 和后续的全部客户端链接的 IO 操做都是在一个线程中处理的. 那么对应到 Reactor 的线程模型中, 咱们这样设置 NioEventLoopGroup 时, 就至关于 Reactor 单线程模型.

多线程模型

同理, 再来看一下下面的例子:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 ...

bossGroup 中只有一个线程, 而 workerGroup 中的线程是 CPU 核心数乘以2, 所以对应的到 Reactor 线程模型中, 咱们知道, 这样设置的 NioEventLoopGroup 其实就是 Reactor 多线程模型.

主从多线程模型

相信读者朋友都想到了, 实现主从线程模型的例子以下:

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 ...

bossGroup 线程池中的线程数咱们设置为4, 而 workerGroup 中的线程是 CPU 核心数乘以2, 所以对应的到 Reactor 线程模型中, 咱们知道, 这样设置的 NioEventLoopGroup 其实就是 Reactor 主从多线程模型.


根据 @labmem 的提示, Netty 的服务器端的 acceptor 阶段, 没有使用到多线程, 所以上面的 主从多线程模型 在 Netty 的服务器端是不存在的.
服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 所以在调用 Java NIO 的 Selector.select 处理客户端的链接请求时, 其实是在一个线程中的, 因此对只有一个服务的应用来讲, bossGroup 设置多个线程是没有什么做用的, 反而还会形成资源浪费.

经 Google, Netty 中的 bossGroup 为何使用线程池的缘由你们众所纷纭, 不过我在 stackoverflow 上找到一个比较靠谱的答案:

the creator of Netty says multiple boss threads are useful if we share NioEventLoopGroup between different server bootstraps, but I don't see the reason for it.

所以上面的 主从多线程模型 分析是有问题, 抱歉.

NioEventLoopGroup 类层次结构

clipboard.png

NioEventLoopGroup 实例化过程

在前面 Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (客户端) 章节中, 咱们已经简单地介绍了一下 NioEventLoopGroup 的初始化过程, 这里再回顾一下:

clipboard.png

点此下载原图

即:

  • EventLoopGroup(实际上是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children 数组, 其大小是 nThreads, 这样就构成了一个线程池

  • 若是咱们在实例化 NioEventLoopGroup 时, 若是指定线程池大小, 则 nThreads 就是指定的值, 反之是处理器核心数 * 2

  • MultithreadEventExecutorGroup 中会调用 newChild 抽象方法来初始化 children 数组

  • 抽象方法 newChild 是在 NioEventLoopGroup 中实现的, 它返回一个 NioEventLoop 实例.

  • NioEventLoop 属性:

    • SelectorProvider provider 属性: NioEventLoopGroup 构造器中经过 SelectorProvider.provider() 获取一个 SelectorProvider

    • Selector selector 属性: NioEventLoop 构造器中经过调用经过 selector = provider.openSelector() 获取一个 selector 对象.

NioEventLoop

NioEventLoop 继承于 SingleThreadEventLoop, 而 SingleThreadEventLoop 又继承于 SingleThreadEventExecutor. SingleThreadEventExecutor 是 Netty 中对本地线程的抽象, 它内部有一个 Thread thread 属性, 存储了一个本地 Java 线程. 所以咱们能够认为, 一个 NioEventLoop 其实和一个特定的线程绑定, 而且在其生命周期内, 绑定的线程都不会再改变.

NioEventLoop 类层次结构

clipboard.png

NioEventLoop 的类层次结构图仍是比较复杂的, 不过咱们只须要关注几个重要的点便可. 首先 NioEventLoop 的继承链以下:

NioEventLoop -> SingleThreadEventLoop -> SingleThreadEventExecutor -> AbstractScheduledEventExecutor

在 AbstractScheduledEventExecutor 中, Netty 实现了 NioEventLoop 的 schedule 功能, 即咱们能够经过调用一个 NioEventLoop 实例的 schedule 方法来运行一些定时任务. 而在 SingleThreadEventLoop 中, 又实现了任务队列的功能, 经过它, 咱们能够调用一个 NioEventLoop 实例的 execute 方法来向任务队列中添加一个 task, 并由 NioEventLoop 进行调度执行.

一般来讲, NioEventLoop 肩负着两种任务, 第一个是做为 IO 线程, 执行与 Channel 相关的 IO 操做, 包括 调用 select 等待就绪的 IO 事件、读写数据与数据的处理等; 而第二个任务是做为任务队列, 执行 taskQueue 中的任务, 例如用户调用 eventLoop.schedule 提交的定时任务也是这个线程执行的.

NioEventLoop 的实例化过程

clipboard.png

点此下载原图

从上图能够看到, SingleThreadEventExecutor 有一个名为 thread 的 Thread 类型字段, 这个字段就表明了与 SingleThreadEventExecutor 关联的本地线程.
下面是这个构造器的代码:

protected SingleThreadEventExecutor(
        EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) {
    this.parent = parent;
    this.addTaskWakesUp = addTaskWakesUp;

    thread = threadFactory.newThread(new Runnable() {
        @Override
        public void run() {
            boolean success = false;
            updateLastExecutionTime();
            try {
                SingleThreadEventExecutor.this.run();
                success = true;
            } catch (Throwable t) {
                logger.warn("Unexpected exception from an event executor: ", t);
            } finally {
                // 省略清理代码
                ...
            }
        }
    });
    threadProperties = new DefaultThreadProperties(thread);
    taskQueue = newTaskQueue();
}

在 SingleThreadEventExecutor 构造器中, 经过 threadFactory.newThread 建立了一个新的 Java 线程. 在这个线程中所作的事情主要就是调用 SingleThreadEventExecutor.this.run() 方法, 而由于 NioEventLoop 实现了这个方法, 所以根据多态性, 其实调用的是 NioEventLoop.run() 方法.

EventLoop 与 Channel 的关联

Netty 中, 每一个 Channel 都有且仅有一个 EventLoop 与之关联, 它们的关联过程以下:

clipboard.png

点此下载原图

从上图中咱们能够看到, 当调用了 AbstractChannel#AbstractUnsafe.register 后, 就完成了 Channel 和 EventLoop 的关联. register 实现以下:

@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 删除条件检查.
    ...
    AbstractChannel.this.eventLoop = eventLoop;

    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new OneTimeTask() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            ...
        }
    }
}

AbstractChannel#AbstractUnsafe.register 中, 会将一个 EventLoop 赋值给 AbstractChannel 内部的 eventLoop 字段, 到这里就完成了 EventLoop 与 Channel 的关联过程.

EventLoop 的启动

在前面咱们已经知道了, NioEventLoop 自己就是一个 SingleThreadEventExecutor, 所以 NioEventLoop 的启动, 其实就是 NioEventLoop 所绑定的本地 Java 线程的启动.
依照这个思想, 咱们只要找到在哪里调用了 SingleThreadEventExecutor 的 thread 字段的 start() 方法就能够知道是在哪里启动的这个线程了.
从代码中搜索, thread.start() 被封装到 SingleThreadEventExecutor.startThread() 方法中了:

private void startThread() {
    if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            thread.start();
        }
    }
}

STATE_UPDATER 是 SingleThreadEventExecutor 内部维护的一个属性, 它的做用是标识当前的 thread 的状态. 在初始的时候, STATE_UPDATER == ST_NOT_STARTED, 所以第一次调用 startThread() 方法时, 就会进入到 if 语句内, 进而调用到 thread.start().
而这个关键的 startThread() 方法又是在哪里调用的呢? 通过方法调用关系搜索, 咱们发现, startThread 是在 SingleThreadEventExecutor.execute 方法中调用的:

@Override
public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }

    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
        startThread(); // 调用 startThread 方法, 启动EventLoop 线程.
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

既然如此, 那如今咱们的工做就变为了寻找 在哪里第一次调用了 SingleThreadEventExecutor.execute() 方法.
若是留心的读者可能已经注意到了, 咱们在 EventLoop 与 Channel 的关联 这一小节时, 有提到到在注册 channel 的过程当中, 会在 AbstractChannel#AbstractUnsafe.register 中调用 eventLoop.execute 方法, 在 EventLoop 中进行 Channel 注册代码的执行, AbstractChannel#AbstractUnsafe.register 部分代码以下:

if (eventLoop.inEventLoop()) {
    register0(promise);
} else {
    try {
        eventLoop.execute(new OneTimeTask() {
            @Override
            public void run() {
                register0(promise);
            }
        });
    } catch (Throwable t) {
        ...
    }
}

很显然, 一路从 Bootstrap.bind 方法跟踪到 AbstractChannel#AbstractUnsafe.register 方法, 整个代码都是在主线程中运行的, 所以上面的 eventLoop.inEventLoop() 就为 false, 因而进入到 else 分支, 在这个分支中调用了 eventLoop.execute. eventLoop 是一个 NioEventLoop 的实例, 而 NioEventLoop 没有实现 execute 方法, 所以调用的是 SingleThreadEventExecutor.execute:

@Override
public void execute(Runnable task) {
    ...
    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
        startThread();
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

咱们已经分析过了, inEventLoop == false, 所以执行到 else 分支, 在这里就调用了 startThread() 方法来启动 SingleThreadEventExecutor 内部关联的 Java 本地线程了.
总结一句话, 当 EventLoop.execute 第一次被调用时, 就会触发 startThread() 的调用, 进而致使了 EventLoop 所对应的 Java 线程的启动.
咱们将 EventLoop 与 Channel 的关联 小节中的时序图补全后, 就获得了 EventLoop 启动过程的时序图:

clipboard.png

点此下载原图

下一小节: Netty 源码分析之 三 我就是大名鼎鼎的 EventLoop(二)

本文由 yongshun 发表于我的博客, 采用 署名-相同方式共享 3.0 中国大陆许可协议.
Email: yongshun1228@gmail .com
本文标题为: Netty 源码分析之 三 我就是大名鼎鼎的 EventLoop(一)
本文连接为: http://www.javashuo.com/article/p-zbirqpbl-ga.html