资讯专栏INFORMATION COLUMN

EventLoop 和 线程模型

cpupro / 1340人阅读

摘要:关于的线程模型首先我们来看一下的线程模型的线程模型有三种单线程模型多线程模型主从多线程模型首先来看一下单线程模型所谓单线程即处理和处理都在一个线程中处理这个模型的坏处显而易见当其中某个阻塞时会导致其他所有的的都得不到执行并且更严重的是的阻塞

关于 Reactor 的线程模型

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

单线程模型

多线程模型

主从多线程模型

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

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

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

Reactor 多线程模型 有如下特点:

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

客户端连接的 IO 操作都是由一个特定的 NIO 线程池负责. 每个客户端连接都与一个特定的 NIO 线程绑定, 因此在这个客户端连接中的所有 IO 操作都是在同一个线程中完成的.

客户端连接有很多, 但是 NIO 线程数是比较少的, 因此一个 NIO 线程可以同时绑定到多个客户端连接中.

接下来我们再来看一下 Reactor 的主从多线程模型.

一般情况下, Reactor 的多线程模式已经可以很好的工作了, 但是我们考虑一下如下情况: 如果我们的服务器需要同时处理大量的客户端连接请求或我们需要在客户端连接时, 进行一些权限的检查, 那么单线程的 Acceptor 很有可能就处理不过来, 造成了大量的客户端不能连接到服务器.

Reactor 的主从多线程模型就是在这样的情况下提出来的, 它的特点是: 服务器端接收客户端的连接请求不再是一个线程, 而是由一个独立的线程池组成. 它的线程模型如下:

可以看到, 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 程序时, 是需要设置 bossGroupworkerGroup 的, 为什么这里就只有一个 bossGroup?

其实很简单, ServerBootstrap 重写了 group 方法:

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

因此当传入一个 group 时, 那么 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)
 ...

服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费.

关于 bossGroup 与 workerGroup

bossGroup 是用于服务端的 accept, 即用于处理客户端的连接请求. 我们可以把 Netty 比作一个饭店, bossGroup 就像一个像一个前台接待, 当客户来到饭店吃时, 接待员就会引导顾客就坐, 为顾客端茶送水等.

而 workerGroup, 其实就是实际上干活的, 它们负责客户端连接通道的 IO 操作: 当接待员 招待好顾客后, 就可以稍做休息, 而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了.

关于 bossGroup 与 workerGroup 的关系, 我们可以用如下图来展示:

首先, 服务器端 bossGroup 不断地监听是否有客户端的连接, 当发现有一个新的客户端连接到来时, bossGroup 就会为此连接初始化各项资源, 然后从 workerGroup 中选出一个 EventLoop 绑定到此客户端连接中. 那么接下来的服务器与客户端的交互过程就全部在此分配的 EventLoop 中了.

NioEventLoop

NioEventLoop 继承于 SingleThreadEventLoop, 而 SingleThreadEventLoop 又继承于 SingleThreadEventExecutor. SingleThreadEventExecutor 是 Netty 中对本地线程的抽象, 它内部有一个 Thread thread 属性, 存储了一个本地 Java 线程. 因此我们可以认为, 一个 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 提交的定时任务也是这个线程执行的.

NioEventLoopGroup

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

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

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

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

NioEventLoopGroup 就像一个线程池, 负责为每个新创建的 Channel 分配一个 EventLoop. 而 EventLoop 就是一个线程, 负责执行用户任务和 IO 事件.
值得注意的是: 执行 IO 事件(自己的业务逻辑)时, 如果当前业务逻辑没有执行完毕, 是无法处理下一个 IO 事件的.
Netty 的任务队列机制

我们已经提到过, 在Netty 中, 一个 NioEventLoop 通常需要肩负起两种任务, 第一个是作为 IO 线程, 处理 IO 操作; 第二个就是作为任务线程, 处理 taskQueue 中的任务.

Task 的添加 普通 Runnable 任务

NioEventLoop 继承于 SingleThreadEventExecutor, 而 SingleThreadEventExecutor 中有一个 Queue taskQueue 字段, 用于存放添加的 Task. 在 Netty 中, 每个 Task 都使用一个实现了 Runnable 接口的实例来表示.
例如当我们需要将一个 Runnable 添加到 taskQueue 中时, 我们可以进行如下操作:

EventLoop eventLoop = channel.eventLoop();
eventLoop.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, Netty!");
    }
});
任务的执行

当一个任务被添加到 taskQueue 后, 它是怎么被 EventLoop 执行的呢?
让我们回到 NioEventLoop.run() 方法中, 在这个方法里, 会分别调用 processSelectedKeys() 和 runAllTasks() 方法, 来进行 IO 事件的处理和 task 的处理.
runAllTasks 方法有两个重载的方法, 一个是无参数的, 另一个有一个参数的. 首先来看一下无参数的 runAllTasks:

protected boolean runAllTasks() {
    fetchFromScheduledTaskQueue();
    Runnable task = pollTask();
    if (task == null) {
        return false;
    }

    for (;;) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception.", t);
        }

        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            return true;
        }
    }
}

在此方法的一开始调用的 fetchFromScheduledTaskQueue() 其实就是将 scheduledTaskQueue 中已经可以执行的(即定时时间已到的 schedule 任务) 拿出来并添加到 taskQueue 中, 作为可执行的 task 等待被调度执行.

private void fetchFromScheduledTaskQueue() {
    if (hasScheduledTasks()) {
        long nanoTime = AbstractScheduledEventExecutor.nanoTime();
        for (;;) {
            Runnable scheduledTask = pollScheduledTask(nanoTime);
            if (scheduledTask == null) {
                break;
            }
            taskQueue.add(scheduledTask);
        }
    }
}

接下来 runAllTasks() 方法就会不断调用 task = pollTask() 从 taskQueue 中获取一个可执行的 task, 然后调用它的 run() 方法来运行此 task.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74907.html

相关文章

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

    摘要:目录源码之下无秘密做最好的源码分析教程源码分析之番外篇的前生今世的前生今世之一简介的前生今世之二小结的前生今世之三详解的前生今世之四详解源码分析之零磨刀不误砍柴工源码分析环境搭建源码分析之一揭开神秘的红盖头源码分析之一揭开神秘的红盖头客户端 目录 源码之下无秘密 ── 做最好的 Netty 源码分析教程 Netty 源码分析之 番外篇 Java NIO 的前生今世 Java NI...

    livem 评论0 收藏0
  • Netty 框架总结「ChannelHandler 及 EventLoop

    摘要:随着状态发生变化,相应的产生。这些被转发到中的来采取相应的操作。当收到数据或相关的状态改变时,这些方法被调用,这些方法和的生命周期密切相关。主要由一系列组成的。采用的线程模型,在同一个线程的中处理所有发生的事。 「博客搬家」 原地址: 简书 原发表时间: 2017-05-05 学习了一段时间的 Netty,将重点与学习心得总结如下,本文主要总结ChannelHandler 及 E...

    VioletJack 评论0 收藏0
  • Netty 4.1 源代码学习:线程模型

    摘要:前言本文以自带的示例工程为例,简要介绍线程模型示例工程的代码位于很简单,仅包含一个方法用于初始化以及,我们来看看其中和线程模型相关的一些代码在的初始化代码中实例化了两个对象和,它们有着公共基类,这个是线程模型的核心类名让人联想到组合模式, 前言 本文以 netty 4.1 自带的示例工程 netty-example 为例,简要介绍 netty 线程模型 EchoServer echo ...

    monw3c 评论0 收藏0
  • JS核心知识点梳理——异步,单线程,运行机制

    摘要:引言学习的时候,经常听人说,即是异步的,又是单线程的。所以我们说是异步单线程的。参考从浏览器多进程到单线程,运行机制最全面的一次梳理运行机制详解再谈异步机制详解运行原理解析并发模型与事件循环 showImg(https://segmentfault.com/img/bVbo4hv?w=1800&h=1000); 引言 学习javascipt的时候,经常听人说,javascipt即是异步...

    TANKING 评论0 收藏0
  • netty学习总结(一)

    摘要:是什么是一个异步的,事件驱动的网络编程框架。责任链模式通过将组装起来,通过向里添加来监听处理发生的事件。相比于的的不仅易用,而且还支持自动扩容。入站入站事件一般是由外部触发的,如收到数据。 netty是什么? netty是一个异步的,事件驱动的网络编程框架。 netty的技术基础 netty是对Java NIO和Java线程池技术的封装 netty解决了什么问题 使用Java IO进行...

    CntChen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<