Netty 线程模型

1. 前言

前面几节分别讲解了 Reactor 的三种线程模型,都知道主从 Reactor 多线程模型的性能非常的好,那么 Netty 是否就是使用主从 Reactor 多线程模型呢?其实 Netty 线程模型是基于主从 Reactor 多线程模型做了一定的改造,Netty 的线程模型要比 Reactor 主从多线程模型还要复杂。本节主要是通过图解的方式逐步分析 Netty 线程模型的原理。

2. Netty 模型介绍

2.1 模型介绍

图片描述

Netty 模型架构说明:

  1. Netty 抽象出两个线程池,分别是 BossGroup 和 WorkerGroup,BossGroup 专门负责接受客户端的连接,Worker 请求处理;
  2. BossGroup 和 WorkerGroup 类型默认使用的是 NioEventLoopGroup;
  3. NioEventLoopGroup 是一个定时任务线程池,NioEventLoop 是真正工作的线程;
  4. 每个 BossGroup 的 NioEventLoop 分别循环执行三个步骤
    4.1 每个 NioEventLoop 都有一个 Selector,并且不断轮询 accept 事件;
    4.2 处理 accept 事件,与客户端建立连接,生成 NioSocketChannel,并且将其注册到某个 WorkerGroup 下的 NioEventLoop 上的 Selector 上;
    4.3 处理任务队列中的任务,即 runAllTasks。
  5. 每个 WorkerGroup 的 NioEventLoop 分别循环执行三个步骤
    5.1 轮询 read 和 write 事件;
    5.2 处理 I/O 事件,即 read,write 事件,并在其对应的 NioSocketChannel 处理;
    5.3 处理任务队列的任务,即 runAllTasks。

2.2 核心概念理解

Tips: 额外知识点补充,这里提前剧透一下 Channel、ChannelPipeline、ChannelHanlder 之间的关系

  1. 每个客户端连接进来的时候,服务端都会建立一个 Channel;
  2. 为每个 Channel 绑定一个 NioEventLoop 线程,该线程主要负责处理该 Channel 的业务,一个 Channel 对应一个 NioEventLoop,但是一个 NioEventLoop 可以同时服务多个 Channel;
  3. 为每个 Channel 绑定一个 ChannelPipeline,它是一个业务管道,专门负责管理业务链,也就是 ChannelHandler;
  4. WorkerGroup 的核心方法是 runAllTasks (),它主要是触发 NioEventLoop 去处理对应的 Channel 里面的 ChannelPipeline 里面的 ChannelHandler 里面的业务逻辑。

3. Netty 模型配置

3.1 单线程配置

在 ServerBootstrap 调用方法 group 的时候,传递的参数是同一个线程组,且在构造线程组的时候,构造参数为 1。

实例:

public class ServerNetty{
    private ServerBootstrap bootstrap=null;
    private EventLoopGroup group=null;
    public void init(){
        group=new NioEventLoopGroup(1);//线程数量为 1
        bootstrap.group(group,group);
    }
}

3.2 多线程配置

在 ServerBootstrap 调用方法 group 的时候,传递的参数是两个不同的线程组,负责监听的 acceptor 线程组的线程数为 1,负责处理客户端线程组的线程数大于 1。

实例:

public class ServerNetty{
    private ServerBootstrap bootstrap=null;
    private EventLoopGroup acceptorGroup=null;
    private EventLoopGroup clientGroup=null;
    public void init(){
        acceptorGroup=new NioEventLoopGroup(1);//线程数量为 1
        clientGroup=new NioEventLoopGroup();//默认是 cpu 的核心数
        bootstrap.group(acceptorGroup,clientGroup);
    }
}

3.3 主从多线程配置

在 ServerBootstrap 调用方法 group 的时候,传递的参数是两个不同的线程组,负责监听的 acceptor 线程组的线程数大于 1,负责处理客户端线程组的线程数大于 1。

实例:

public class ServerNetty{
    private ServerBootstrap bootstrap=null;
    private EventLoopGroup acceptorGroup=null;
    private EventLoopGroup clientGroup=null;
    public void init(){
        acceptorGroup=new NioEventLoopGroup();//默认是 cpu 的核心数
        clientGroup=new NioEventLoopGroup();//默认是 cpu 的核心数
        bootstrap.group(acceptorGroup,clientGroup);
    }
}

4. 自定义任务队列

通常情况下,任务队列中常见的任务主要有以下几种类型:

  1. 用户自定义的异步任务,比如:依赖线程池去异步某个任务等;
  2. 用户自定义的定时任务,比如:依赖定时线程池去定义每隔 n 秒执行某个任务等;
  3. 非当前 reactor 线程调用 channel 的各种方法。

4.1 异步任务

其实跟我们平时使用线程池没有什么区别,只不过调用的是底层 Netty 线程组。

实例:

//使用 reactor 线程的异步任务
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        //...
    }
});

//使用线程池去实现异步任务
ExecutorService es = Executors.newFixedThreadPool(5);
es.execute(new Runnable() {
    @Override
    public void run() {
        
    }
});

4.2 定时任务

其实类似我们平时使用的定时任务线程池(如:ScheduledThreadPool),只不过是调用底层 Netty 线程组。

实例:

//使用 reactor 线程实现的定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {

    }
}, 60, TimeUnit.SECONDS);

//使用线程池去实现定时任务
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
ses.schedule(new Runnable() {
    public void run() {
        System.out.println("i:" + temp);
    }
}, 3, TimeUnit.SECONDS);

总结:

  1. 当前 reactor 线程调用当前 eventLoop 执行任务,直接执行,否则,添加到任务队列稍后执行;
  2. netty 内部的任务分为普通任务和定时任务,分别落地到 MpscQueue 和 PriorityQueue;
  3. netty 每次执行任务循环之前,会将已经到期的定时任务从 PriorityQueue 转移到 MpscQueue;
  4. netty 每隔 64 个任务检查一下是否该退出任务循环。

5. 小结

本节主要掌握的核心知识点

  1. Netty 的模型的理解,以及每个 NioEventLoop 所执行的三个核心操作,分别是①轮询出 IO 事件;②处理 IO 事件;③处理任务队列;
  2. 了解 Channel、ChannelPipeline、ChannelHandler 之间的关系,以及 NioEventLoop 主要负责处理每个 Channel 的业务逻辑;
  3. Netty 如何配置三种 Reactor 模型;
  4. 如何使用内置的 NioEventLoop 执行自定义的异步任务和定时任务。