NioEventLoopGroup与Executor

539 阅读7分钟

EventLoop

  • 事件循环机制的基本原理是,程序通过一个循环不断地等待事件发生,当事件发生时,立即调用相应的处理函数进行处理。事件循环机制的核心是事件循环(Event Loop),它负责监听事件、调用回调函数、处理事件等操作。在事件循环机制中,当一个事件被触发时,会将它放入一个事件队列(Event Queue)中,事件循环会不断地从事件队列中取出事件并处理它们。
  • 总之,就是处理任务的。

何谓EventLoop

  • EventLoop是一种事件循环机制,主要用于实现基于事件驱动的异步编程模型。在Java中,Netty框架的核心就是EventLoop,它负责管理所有的IO事件,比如连接、读写等操作,并且基于NIO的Selector机制实现了高效的事件分发和处理
  • EventLoop通过不断地轮询IO事件,将已经就绪的IO事件分发给对应的ChannelHandler进行处理。因此,EventLoop实际上是一个事件循环机制,它不断地从就绪的IO事件中选择一个进行处理,直到所有的IO事件都被处理完毕。
  • EventLoop通常会被封装在一个线程中运行,它维护了一个任务队列,所有的任务都会被提交到该队列中。EventLoop在运行过程中,会不断地从任务队列中取出任务进行处理,如果任务队列为空,EventLoop会进入休眠状态,等待新的任务被提交。 image.png

何谓EventLoopGroup

image.png

  • EventLoopGroup 是 Netty 中的一个重要组件,它可以看作是 EventLoop 的集合,它的主要作用是管理和分配 EventLoop。它实现了一种负载均衡的策略,可以将连接分配到不同的 EventLoop 上进行处理,从而避免单一的 EventLoop 被过度占用而导致性能瓶颈。
  • 同时,EventLoopGroup 还可以在多线程场景下提供一些便利。在使用多线程的情况下,可以将不同的 EventLoop 分配到不同的线程中进行处理,从而实现更高效的并发处理能力。

  • ChannelFuture表示一个尚未完成的异步操作的结果。当你发起一个异步操作(如写数据到Channel),Netty会立即返回一个ChannelFuture实例,它会在操作完成后设置结果或者发生错误时通知你。你可以通过添加监听器或者通过阻塞等待来处理结果。
  • Future详见Java 并发之Future模式(异步) - 掘金 (juejin.cn)

组合模式

可以发现,EventLoop是EventLoopGroup的子类,而EventLoop又是EventLoopGroup的成员变量。

  • 组合模式是一种结构型模式,它将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端可以以一致的方式处理单个对象以及对象组合。
  • 经典的就是文件系统了,文件夹中包含文件和文件夹,这就会形成一种树结构。
  • 在Netty中,EventLoop看成是文件,那EventLoopGroup是文件夹,又因为EventLoop是Group的子类,那它也可以是文件夹。

EventLoopGroup的实现类

NioEventLoopGroup

image.png

  • 实例化时,会调用父类的构造方法。 image.png
  • 主要是初始化的成员变量 image.png
    • EventLoop接口继承EventLoopGroup,EventLoopGroup继承自EventExecutor接口,也就是说,EventLoop本质上也是一个EventExecutor
  • 最好在实例化时指定线程数 image.png
    •  EventLoopGroup bossGroup = new NioEventLoopGroup(1);
      

Executor接口

  • 线程池和Netty的EventLoopGroup的老父亲都是它。
  • 就是个执行任务的组件。
  • NioEventLoopGroup类关系图如下: image.png

理解Executor

阅读它的注释即可,就是一些常见的实现方式。

  • Executor 接口提供了一种将任务提交与任务如何运行的机制解耦的方式,包括线程使用、调度等细节。通常情况下,我们使用 Executor 而不是显式地创建线程来执行任务。比如,我们可以使用以下方式执行一组任务:
    Executor executor = Executors.newCachedThreadPool();
    executor.execute(new RunnableTask1());
    executor.execute(new RunnableTask2());
    
    • 解耦的是线程的创建和任务的提交,这两个动作,换句话说,提交任务和创建线程的代码不要写一块。
  • Executor 接口并不严格要求执行必须是异步的。在最简单的情况下,一个执行程序可以立即在调用者的线程中运行提交的任务:
    class DirectExecutor implements Executor {
        public void execute(Runnable r) {
             r.run();
        }
    }
    
    • 相当于普通的方法调用。
  • 许多 Executor 实现对任务的调度方式和时间施加了某种限制。下面的执行程序将任务的提交序列化到第二个执行程序中,展示了组合执行程序:
    class SerialExecutor implements Executor {
      final Queue<Runnable> tasks = new ArrayDeque<>();
      final Executor executor;
      Runnable active;
    
      SerialExecutor(Executor executor) {
        this.executor = executor;
      }
    
      public synchronized void execute(Runnable r) {
        tasks.add(() -> {
          try {
            r.run();
          } finally {
            scheduleNext();
          }
        });
        if (active == null) {
          scheduleNext();
        }
      }
    
      protected synchronized void scheduleNext() {
        if ((active = tasks.poll()) != null) {
          executor.execute(active);
        }
      }
    
    • 是将任务按顺序添加到队列中,并在每个任务执行完成后再执行下一个任务。这种方式可以保证任务按照提交的顺序依次执行,而不会并发地执行多个任务。
  • 该接口提到了“内存一致性效果”(memory consistency effects)这个概念,说明在向Executor提交任务之前,线程中的操作(Actions)在任务执行开始之前(perhaps in another thread)将会被“happen-before”关系所限制,也就是说之前的操作将保证在之后的操作之前完成,以此确保并发操作的正确性。
    • 假设有一个多线程程序,线程A和线程B都需要访问一个共享变量x。如果线程A在执行完某些操作后,将一个任务提交给Executor,并将x的值设为1,而线程B也访问了x,那么线程B读取到的x的值是否为1,就是不确定的了。因为线程B读取x的值发生的时间不确定,可能在线程A将任务提交给Executor之前,也可能在任务执行之后。但是,由于Executor保证提交任务之前的操作happen-before任务执行的开始,所以如果在任务执行之前线程A将x的值设为1,那么线程B一定会读取到x的最新值1,而不会出现脏读的情况。
    • happen-before”关系详解:Java 并发之volatile关键字 - 掘金 (juejin.cn)
  • 总结:
    • Executor就是处理提交任务接口。
    • 决定处理任务的方式,可以是谁提交谁执行,可以是异步,可以按提交顺序,可以并发执行。
    • 那这样看来,Netty的EventLoopGroup,也会包含处理任务的一种具体方式。
    • 解耦任务提交和任务的执行(线程的创建和管理)。

线程工厂

  • 线程就是执行任务的最终对象,所以需要创建线程,为了将创建线程单独封装,遂需要线程工厂。 image.png
  • 线程工厂的设计初衷是为了提供一种可定制的方式来创建线程,控制线程的创建方式、线程的名称、线程的优先级等属性。
  • 将创建线程单独封装,隐藏new Thread()这个动作。
    • 它将创建新线程的逻辑从线程的调用方中分离出来,提高了代码的可维护性和可扩展性。

NioEventLoopGroup默认的Executor

  • ThreadPerTaskExecutor image.png
  • 命令模式:
    • 这段代码中的 execute() 方法实际上使用了命令模式。它接收一个 Runnable 对象作为参数,并将其封装在一个新线程中执行。这里的 Runnable 对象就扮演了一个命令的角色。
  • 委托模式:
    • ThreadPerTaskExecutor将任务的执行委托给了ThreadFactory对象。

小结

  • 实例化NioEventLoopGroup对象,只是为netty的工作做一些准备,比如准备好执行器。
  • 此时并没有线程的创建。