两篇文章看懂EventLoopGroup,EventLoop的设计和运行机制(二)

前言

上一篇更多的是说结论,那结论是怎么来的呢?也是一步一步学习出来的,我在学习过程中,使用了Xmind,用来做思维导图还是比较好的,层次结构以及每个方法都可以很好的记录。本文重点分享一下下面这个类图以及每个类中的实现细节:
在这里插入图片描述

EventExecutorGroup和EventExecutor

这两个接口是上述类图中的基础,也是EventLoop的基础,特别是EventExecutorGroup,是netty自定义的第一个接口,先学习好它们,也就能学习好EventLoop了。
还是按照我的思路,先删减,后增补。

EventExecutorGroup

先看一下拆分的类图:
在这里插入图片描述
它本身继承了4个重要接口,以及它们各自的功能:

  • Iterable:迭代器接口,返回一个迭代器。说明已经具备了Group的管理或者容器功能。
  • Executor:具备了提交任务的能力。
  • ExecutorService:具备了线程池的能力。
  • ScheduledExecutorService:具备了执行调度任务的能力。

这4种能力不多说,都是JDK本身提供的,我们杰西莱看一下,它本身定义了哪些方法呢?如下图:
在这里插入图片描述
虽然有很多方法,但大多数都是上面接口的,重新写了一遍而已,我们对它新加的一些方法做一些分析。

  • 它优化了JDK提供的关于中断的方法:标记过时(shutdown,shutdownNow),并且新加了几个方法
    • boolean isShuttingDown();
    • Future<?> shutdownGracefully();
    • Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit);
  • 增加了一个中断状态监听的方法
    • Future<?> terminationFuture()。注意这个Future是netty的.
    • 可以在这个future上面增加一些监听器(addListener),然后当中断的时候就会触发。
  • 增加了next方法。返回的是由它管理的EventExecutor。这个组的味道已经出现了。这个也是后面实现轮询内部成员的一个关键方法。
    这个接口的功能就是:线程池,管理一组EventExecutor,调度任务,中断监听。

我们单从Group的功能来看的话,这个接口下面就再也没有其它接口了,都是一些抽象类了,下面是一个比较简单的类图:
在这里插入图片描述

AbstractEventExecutorGroup

这个类没有任何的实现,仅仅只是把所有的提交任务的方法做了一下默认实现:就是调用一些next()方法轮询出来一个Executor然后去执行,比如:

  @Override
    public void execute(Runnable command) {
        next().execute(command);
    }

其它的所有方法都没有实现,也确实挺抽象的。
不过这个抽象类的作用还是非常大的,之前在分析组和成员关系的时候,组也具备了一些成员关系的功能,但是它的执行就是通过成员去完成的,而且这些功能本身也确实不需要组去关心的。而netty就是把这些功能放在了这个类里面,也放的特别合适,它没有实现任何有关于的组的管理功能,实际上就是交给子类去完成了,而子类在实现管理功能的时候,也就不需要关心这些功能了(提交任务),父类都已经实现了。

MultithreadEventExecutorGroup

听名字就可以听出来一些味道了:多线程事件执行器组。
3个关键词:多线程,事件执行器,组。
这个里面才是真正实现了组的管理功能,看一下它内部所有的属性和方法:

在这里插入图片描述
先看一下内部属性吧:

  • EventExecutor[] children:固定大小的EventExecutor数组,构造器里面会根据大小全部初始化成员。
  • EventExecutorChooser chooser:成员轮询器。虽然有两种实现,但其实都是按照(0,1,2,3,…,n-1)的顺序来的。只不过当大小是2的幂的时候,采用了一下位的和(&)运算,会稍微快一些。这个就是next方法的实现。
  • terminationFuture:中断的一个Future,可以用来监听中断事件。
  • readonlyChildren:用一个不可写的集合来做迭代器用。
  • terminatedChildren:原子int,用来标记当前有多少个成员已经被中断了,当所有的都被中断的时候,就会触发中断事件。

这个类比较核心的就是它的构造器了,至于其它的关于中断的方法,基本上也都是循环中断所有的成员,不是特别复杂。当然还有一个创建成员的抽象方法,不过也是和构造器相呼应的。

关于它的构造器和一个抽象方法,我觉得也是比较有趣的点。

  • 它的所有的构造器都是protected的,说明不是对外开放的,由子类去调用的。

  • 最有趣的在于构造器的最后一个参数居然是:Object… args。在构造器里面,这种写法确实还是比较少见的。

  • 这个类仅有一个抽象方法:

    protected abstract EventExecutor newChild(Executor executor, 	Object... args) throws Exception
    

    就是在初始化成员的时候需要调用的方法,而且它的最后一个参数也是 Object… args。其实构造器的对象数组的入参和这个入参是相呼应的,就是同一个。

  • 这个构造器当时在定义的时候,开发人员估计费了不少的心思啊,哈哈。

  • 那为什么要这样写呢?那肯定是为了复用,复用这些组的管理功能。它所管理的Executor到底需要哪些参数,以及如何来创建。它不关心,它也没法关心;所以留到了子类,同时它希望对于具体的子类而已,那个对象数组不能暴露给框架的使用人员,希望他们具体化,不能这么抽象的使用,所以构造器全部protected。

MultithreadEventExecutorGroup到这边就结束了,它更多的是完成了成员的初始化以及中断的相关内容。但是成员EventExecutor到底什么?且看下面的分析。

DefaultEventExecutorGroup

这个类非常简单,提供了3个构造器,实现了newChild方法,并且明确了成员就是DefaultEventExecutor。
当我学习到这边的时候,就基本非常明确了,它就是一个线程池,我第一个想到的就是和ThreadPoolExecutor进行比较,之前的文章以及比较过了。但是在比较之前,想了一下,我好像还并不知道任务是怎么执行的,也就是EventExecutor的具体实现了。

EventExecutor

这个接口才是真正的执行任务的接口:
在这里插入图片描述
它继承了EventExecutorGroup,说明它也拥有上面说的所有功能。
重点关注一下,它新定义的方法吧:
在这里插入图片描述
它基本上新定义了3类接口:

  • parent():返回父节点。也反映了它被EventExecutorGroup管理的特性。
  • inEventLoop():判断当前线程是不是当前EventExecutor所关联的事件循环的线程。非常重要的方法,好多地方都会用到。因为在后续的提交任务的时候,有可能是事件循环的线程(这个就是提交的任务在执行过程当中又提交了新的任务),有可能是其它线程。然后可能需要做一些线程安全方面的工作。
  • 创建Promise,或者Future的方法。这个我没有用过,但是也了解了一些,简单说一下,可能不太对。这两种对象都代表的是异步执行的结果,前者相对于后者具备了写的功能,后者只可读。但是他们都具备结束然后事件通知的能力,那么谁来通知呢?就是当前EventExector所关联的线程去通知。也就是通过它创建的,都会用它的关联线程去执行通知任务。但是具体的应用场景还不太清楚。

以后就是它比Group多出来的方法,一方面体现了它是成员角色,另一方面它也可以做额外的事情。从下面开始,也就没有接口定义了,都是具体的类了。

然后再看一下DefaultEventExecutor的整体类图:
在这里插入图片描述

AbstractEventExecutor

这个类一方面实现了EventExecutor,另一方也继承了JDK提供的AbstractExecutorService。后面这个类更多的是提供提交一些任务的默认实现,也没有做具体的业务实现。
AbstractEventExecutor它本身也没有做太多事情,只是把接口和抽象类整合在了一起。稍微梳理一下吧:
在这里插入图片描述

  • parent:定义了父对象。

  • selfCollection:返回一个属于自己的迭代器。

  • promise,future的方法都做了实现,如下:

    @Override
    public <V> Promise<V> newPromise() {
        return new DefaultPromise<V>(this);
    }
    
    @Override
    public <V> ProgressivePromise<V> newProgressivePromise() {
        return new DefaultProgressivePromise<V>(this);
    }
    
    @Override
    public <V> Future<V> newSucceededFuture(V result) {
        return new SucceededFuture<V>(this, result);
    }
    
    @Override
    public <V> Future<V> newFailedFuture(Throwable cause) {
        return new FailedFuture<V>(this, cause);
    }
    
  • 关于定时任务的,都直接抛了异常,不支持。

  • 提供了一个EventExecutorGroup的一个构造器。

没有做太多的事情,做一些默认实现。重点看一下它的子类。

AbstractScheduledEventExecutor

看这个名字基本也能猜出来,它实现了调度任务:
在这里插入图片描述

  • 内部有一个优先级队列,最早执行的会放在队首,每次提交任务的时候会进行调整。它这个不是线程安全的队列。因此它在添加调度任务的时候,如果不是事件循环线程的话,会提交一个新的普通任务取提交任务,保证线程安全:

    <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
        if (inEventLoop()) {
            scheduledTaskQueue().add(task);
        } else {
            execute(new Runnable() {
                @Override
                public void run() {
                    scheduledTaskQueue().add(task);
                }
            });
        }
    
        return task;
    }
    
  • 所有被提交的调度任务都会被封装成ScheduledFutureTask,这个对象里面有几个关键点:

    • 当这个类被内存加载的时候,会生成当前时间的一个纳秒时间戳。以后所有的时间计算都会以它作为开始时间的标准,比如获取当前纳米戳=获取当前纳秒时间戳的然后减去开始时间就是当前时间:

      private static final long START_TIME = System.nanoTime();
      
      static long nanoTime() {
          return System.nanoTime() - START_TIME;
      }
      
    • deadlineNanos:这个任务应该被执行的纳秒时间戳。都是通过比较这个属性值来判断当前任务是不是到时间去执行了。

    • periodNanos:调度任务的执行周期。为0的话,代表只执行一次。否则,就认为是周期不断执行的任务,间隔就是periodNanos。

    • 关键在于它的run方法,解决了如何执行周期任务。当一个周期任务执行完了,它会把periodNanos加到deadlineNanos上面取,作为新的执行时间,然后重新加入队列。然后就可以再执行了:

      @Override
          public void run() {
              assert executor().inEventLoop();
              try {
                  if (periodNanos == 0) {
                      if (setUncancellableInternal()) {
                          V result = task.call();
                          setSuccessInternal(result);
                      }
                  } else {
                      // check if is done as it may was cancelled
                      if (!isCancelled()) {
                          task.call();
                          if (!executor().isShutdown()) {
                              long p = periodNanos;
                              if (p > 0) {
                                  deadlineNanos += p;
                              } else {
                                  deadlineNanos = nanoTime() - p;
                              }
                              if (!isCancelled()) {
                                  // scheduledTaskQueue can never be null as we lazy init it before submit the task!
                                  Queue<ScheduledFutureTask<?>> scheduledTaskQueue =
                                          ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue;
                                  assert scheduledTaskQueue != null;
                                  scheduledTaskQueue.add(this);
                              }
                          }
                      }
                  }
              } catch (Throwable cause) {
                  setFailureInternal(cause);
              }
          }
      
  • 同时它也提供了一些取出到了执行时间任务的一些方法,供子类使用。

虽然它实现了提交定时任务,以及如何来完成周期任务的执行,但是要怎么触发并且执行它们呢?它没说。

SingleThreadEventExecutor

单线程事件执行器,意思以及很明确了,用一个线程去执行所有的事件。
这个类的属性和方法有点多,生成的图不好看,IDEA也不能删除其中几个。那就没有图了,调几个重要的:

private final Queue<Runnable> taskQueue;
private volatile Thread thread;
private final boolean addTaskWakesUp;
private final int maxPendingTasks;

简单说一下这几个属性吧:

  • taskQueue:任务队列,所有提交的任务会执行扔到这个队列里面。注意它的定义并不是阻塞队列。但是它创建的时候,提供的默认实现就是阻塞队列,并且它提供的一个内部方法takeTask(),取任务的时候还强制要求必须是阻塞队列,否则就抛异常。刚开始还挺郁闷的,想不明白。最后发现创建队列的方法被NIOEventLoop给重写了,它那边提供的是一个无锁高性能队列MPSC(多生茶这,单消费者,这就是它的消费模型)队列,这个并不是阻塞队列。这或许解释得通?
  • thread:事件循环的线程。会在线程启动以后赋值这个变量,它是用Executor启动的,而不是直接一个线程。
  • addTaskWakesUp:如果为true,意味着:当且仅当执行addTask(Runnable)方法的时候,会唤醒执行器的线程。我对这个变量没有搞太明白,它这边提供的构造器默认是true,而EventLoop那边变成了false,而且我很明确它为false的时候提交一个新任务进来才可以唤醒正在阻塞轮询的线程。它说明中的addTask方法,也是仅仅是提交任务的时候会去触发:
    @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }
    
        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }
    
        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }
    
    重点可以看一下后面两行,它必须为true,才会去执行wakeUp方法。所以对于它的描述,实现是看不明白。不解释了,有明白的可以分享一下。
  • maxPendingTasks:队列的最大容量。

从功能上来讲,这个类主要做了这么几件事情:

  • 任务队的创建。
  • 循环线程的启动,当提交第一个任务的时候就会启动线程:
private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                try {
                    doStartThread();
                } catch (Throwable cause) {
                    STATE_UPDATER.set(this, ST_NOT_STARTED);
                    PlatformDependent.throwException(cause);
                }
            }
        }
    }
private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }

                boolean success = false;
                updateLastExecutionTime();
                try {
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                }
               //其它的省略一下
           }
        });
    }

启动以后,它重点就做了一个事情,执行run方法。这是个抽象方法。也就是真正的循环体这边也没有实现。

  • 任务的提交。实现了execute方法,这也是非常关键的方法。
  • 提供了一些从队列里面取任务的方法,供子类用,毕竟它定义队列成private了,比如:
    • Runnable pollTask()
    • Runnable pollTaskFrom(Queue taskQueue)
    • Runnable takeTask()
    • boolean fetchFromScheduledTaskQueue()
    • Runnable peekTask()
  • 还提供了一些执行任务的方法,比如:
    • boolean runAllTasks()
    • runAllTasksFrom(Queue taskQueue)
    • runAllTasks(long timeoutNanos)

它的职责就这些,该做的都做了,但是我要怎么循环取任务呀,好像也没说。这就剩下最关键的一个方法run了。它也是EventLoop最大的区别了。

DefaultEventExecutor

默认的事件执行器。

它实现了run方法,无限循环:从队列里面取出每一个任务,然后去执行:

    @Override
    protected void run() {
        for (;;) {
            Runnable task = takeTask();
            if (task != null) {
                task.run();
                updateLastExecutionTime();
            }

            if (confirmShutdown()) {
                break;
            }
        }
    }

然后就完了,这个类其实也挺简单的。
到此,成员Executor就结束了。不知道了有没有讲清楚,可能也有一些细节都忽略掉了,不过整体它的运行机制应该都讲到了。
话说写到这边的话,它的篇幅已经超过了第一篇了,没有想到写了这么多。主角EventLoop都还没有上场呢。。。还是那句话,这个整明白了,EventLoop也就比较容易了,事实也是如此。

EventExecutorGroup与EventExecutor

上面一直在说这两个,说完了,我再放一张图吧,不解释了,上篇文章里面有:
在这里插入图片描述
我是按照类图的递进关系来学习和讲解的,每个类的职责都会说到,虽然我内心是比较明白的,但是总感觉好像缺了一张有关于每个类的职责的循序渐进图,可以很明显的表现出它们之间的关系,我也没有画。
不过话说回来,对于我们学习者而言,结论重要还是它的整个学习过程重要呢?我的学习初衷就是想了解它的运行机制,仅此而已。而关于类的职责划分,或许这是当时框架的开发者去思考的,但确实我们学习的人也能站在它们的角度去思考,更方面我们的理解,但是,一定要把握好度,差不多就行了。因此,上面那个图,有了更好,没有也行。

接下来看一下重头戏吧。

EventLoopGroup和EventLoop

从我的学习过程看,当学习完上面两个,这边的内容已经是特别少了,主要增加了通道(Channle)和多路复用器(Selector)的内容。
放一张NIOEventLoopGroup和NIOEventLoop的类图吧:
在这里插入图片描述

EventLoopGroup

直接看一下EventLoopGroup到NIOEventLoopGroup的类图吧:
在这里插入图片描述
EventLoopGroup,它继承了EventExecutorGroup,所以我们上面讲的功能它都具备。
看一下它的接口定义:
在这里插入图片描述
4个方法:

  • next():覆盖了父类的方法,把返回值换成了EventLoop。
  • register(Channel channel):注册通道。
  • register(ChannelPromise promise):注册通道。
  • register(Channel channel, ChannelPromise promise):废弃的注册通道的方法。

注意它的第一个注册的实现还是调用了第二个方法,因此说白了,它就增加了一个方法:注册通道。没了。
关于这个方法多说两句哈,使用NIO编程的时候,我们需要使用创建一个SocketChannel,然后再把它注册在多路复用器上面。而netty也是通过这个方法把通道绑定在了EventLoopGroup所管理的其中一个EventLoop中关联的多路复用器上面。和java的nio结合了起来。但其实EventLoop接口本身并没有定义有关多路复用器相关的操作;因此在实现这个方法的时候,AbstractNioChannel所依赖的就是很明确的NIOEventLoop了,属性定义虽然是接口,但是用的时候进行强行类型转换了。这个其实还是挺郁闷的。
EventLoop上面为什么不定义和多路复用器相关的方法。这个可能目前能想来一点点,因为能看见的它的实现有好几种(还没有研究它们有什么用),但只有NIOEventLoop使用到了多路复用器。
突然提到了EventLoop,剧透一下,它继承了EventLoopGroup,然而一个新的方法都没有定义。
Channel中的属性为什么没有直接定义成NIOEventLoop呢?在子类AbstractNioChannel中直接强转了,这边代码看得少,再看看或许会有答案的。

不过就算想不明白,也不用去纠结这个。因为完全不影响整体的一个分析。

MultithreadEventLoopGroup

这个类做的事情更少,就做了一个事情:把MultithreadEventExecutorGroup(上面讲过了)和EventLoopGroup结合在了一起。
那些注册通道之类的也都是调用next()方法让EventLoop去做了。
额外还有个事情,它把这个里面的线程的优先级设为了10(最高),MultithreadEventExecutorGroup这个里面只有5。也是为了优先处理IO请求吧。

NioEventLoopGroup

实现了newChild方法,创建成员的时候,返回的就是NioEventLoop。

    @Override
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }

增加了几个方法:

  • setIoRatio:设置所有的成员的IO比例
  • rebuildSelectors:重置所有成员的多路复用器
    这两个方法没有定义在接口上面,可能是因为最后的实现类里面才有了这两个方法。

然后就没了~~~突然觉得好简单啊,确实就这么简单。

EventLoop

来个NIOEventLoop的简单类图瞅一下:
在这里插入图片描述
它继承了EventLoopGroup和EventExecutor(上面讲过了),但是如我上面所说的,它啥都没有定义,看看:

public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
    @Override
    EventLoopGroup parent();
}

就没了,所以也没有可讲的了。

SingleThreadEventLoop

它继承了SingleThreadEventExecutor(上面讲过了),实现了EventLoop,也就是把两者结合在一起了。
它实现了注册通道的方法:

    @Override
    public ChannelFuture register(Channel channel) {
        return register(new DefaultChannelPromise(channel, this));
    }

    @Override
    public ChannelFuture register(final ChannelPromise promise) {
        ObjectUtil.checkNotNull(promise, "promise");
        promise.channel().unsafe().register(this, promise);
        return promise;
    }

看这个代码,真的是什么都看不出来。
写过NIO代码的应该就会理解,具体的注册逻辑交给了通道本身,这个和jdk的实现策略是一致的。它一行代码内部肯定会执行NIO的原生代码的。

增加了一个看着不知道有什么用的功能,增加了tailQueue,也可以往里面加一些任务,在每次事件轮询之后执行。不知道有啥用。

好像也没有其它重点内容了。越来越简单了。

NIOEventLoop

这个类是真的一点都不简单,毕竟加入了多路复用器,不过我不详细讲了,因为网上讲得非常多,也非常细,我还是强调一些关键点吧。
(我直接抄的第一篇里面的)

  • EventLoop里面关键属性有两个,多路复用器Selector和任务队列。可以把通道(Channel)注册在多路复用器上面,可以不断轮询其中的事件然后执行。任务队列存储提交的task。
  • EventLoop处理的事件(叫任务也行,事件更加贴切吧)整体上有两种:
    • IO事件。当一个EventLoop所关联的多路复用器上面注册的通道发生“连接、接收(Acceptor)、读、写”事件的时候,就相当于触发了IO事件。一般也就两种场景:作为server端的时候,监听一个端口,别人来访问你的端口,就会先触发接收事件,然后读取,写入事件。作为client端的时候,要和目标连接,连接成功以后就会触发连接事件,然后写入,读取事件。(场景简化了一下)
    • 非IO事件。这边又分为两种:
      • 普通任务。使用execute提交的任务,直接执行的。
      • 调度任务。使用schedule提交的任务,一般需要延迟或者周期性执行的。
  • EventLoop在执行的时候,也是无线循环,循环体内主要有3件事:阻塞轮询、执行IO事件和执行非IO事件。
    • 若当前没有任务非IO事件(普通任务)需要执行,且在0.5s内没有需要执行的调度任务的时候,先会进入一个无限循环,里面会调用多路复用器的select(long)方法进行阻塞超时轮询,阻塞超时默认是1s或者有定时任务的话,就取定时任务应该执行的时间与当前时间的间隔为超时时间(意思就是,我超时结束的时候,最早的定时任务刚好可以执行了)。
    • 多路复用器的阻塞超时轮询,并不会一直等到超时,有多种方式可以唤醒它:
      • 多路复用器已经准备好了至少一个事件;基本上就是有IO事件的话,就直接返回了,不会阻塞。
      • 使用wakeup方法。当其它线程调用的时候,会立刻唤醒正在阻塞轮询多路复用器的线程。而EventLoop也是利用了这一点,当有新的任务提交进来,并且当前情况满足4个条件的话,就会执行wakeUp。条件很好满足。而且其中某些条件就是在判断是不是在做阻塞轮询,如果是的话,才会去唤醒。
      • 当正在阻塞轮询的时候,有新的非IO任务进来的话,就会立刻唤醒。和上一点是一回事,换了一种说法。
      • 这边也有一个骚操作,它在执行一些中断操作的时候,会提交一个空任务来唤醒。
      • 超时时间到。
      • 当前线程被中断。后面这两种没有什么可说的。
    • 它的这种唤醒机制,保证了不会影响到任何事件。但是仔细想想,这也是应该的,毕竟是它实在没有事情做的时候,才回去阻塞轮询,因为对于NIO而已,根本不需要进行阻塞,你去忙你的,忙完了回来叫我,我都给你准备好了,你忙你的,我做我的,相互不影响(你=eventLoop,我=多路复用器)。正因为如此,它的代码实现上面,对于跳出无限阻塞轮询(阻塞轮询外层有个无限循环)的条件也是非常开放(不知道怎么描述了),很容易就跳出了,可以看看代码。
    • 阻塞轮询完了或者根本不需要阻塞轮询的(有非IO事件),就要处理事件了。它这边有个IO比例,默认是50,就是IO:非IO=50:50,比如处理IO的时间是100ms,那么处理非IO的时间最大也得是100ms,但是它并没有强行去限制,也确实不好做。它仅仅只是在每执行64个非IO事件以后去判断一下这个时间,超了的话,就停下来。64,也不知道是怎么定义的,说实话我觉得挺多的,太小的话,是不是就会影响到非IO任务的执行了呢?还有这个IO比例,当=100的时候,就完全忽略了时间比,每轮询一次,就会把剩余的所有非IO全部执行完。既然都是IO比例了,这种情况就不应该是只执行IO吗?只执行IO肯定不对,但是这个实现和对应的情况实在是不搭呀,理解不了。或许是因为有些事情我还没有理解透彻。
    • 执行IO的时候,就是把所有轮询到的事件,挨个去执行。这块就是我开篇提到的第四个核心,不过我还没有细看(主要是挺复杂的,不花点事情是搞不明白的),就不说了。反正是一个一个执行IO事件,而且肯定是用当前线程去执行,但是肯定不会花太多时间去处理完的,到最后一定会交给另外一个EventLoopGroup,这也是标准的Reactor模型。netty服务端启动的时候,需要提供两个EventLoopGroup,也是这个作用吧,我猜的。
    • 执行非IO的时候,先把调度队列中所有到期的取出来放进任务队列中,然后挨个去执行。一个是全部执行完,一个有时间限制。执行完了以后,会执行tailTasks队列里面的任务,这个设计不知道用来干嘛的,意思就是每一次轮询结束,就去执行一下。感觉没有什么用呀。
    • 结束以后,下一波轮询又开始了。
  • 它再内部阻塞轮询多路复用器的时候,也修复了JDK的epoll bug。
    • bug描述:它会导致Selector空轮询,IO线程CPU 100%,严重影响系统的安全性和可靠性。
    • 修复思路:
      • 根据该BUG的特征,首先侦测该BUG是否发生:正常情况下,开始时间+阻塞轮询时间<=当前时间;这个是正常的;但是如果反过来的话,就不正常了。实际上阻塞的时间比预期的时间会小,不符合javadoc的描述,就认为做了一次空轮询。当空轮询次数超过默认值512次时,就去重新构建多路复用器。
      • 将问题Selector上注册的Channel转移到新建的Selector上
      • 老的问题Selector关闭,使用新建的Selector替换
  • 它内部还有一个比较重要的原子性的布尔值:wakeUp。它是用来确定是否需要唤醒正在使用阻塞轮询多路复用器的线程(就是EventLoop的线程)。
    • true:代表应该被唤醒或者已经被唤醒了(它有的地方会判断为ture的时候,会立即唤醒,之后也不会修改它的状态)
    • false:代表应该去阻塞轮询了或者正在阻塞轮询。
    • 修改的它的位置有3个:
      • 开始打算轮询的时候,会置为false(select(boolean)方法)。代表我马上要阻塞轮询了。
      • 在无限轮询的循环体内,每次都会判断:有新任务并且是false的时候,会置为true,然后跳出。这个应该是来解决,当添加任务不满足4个条件的时候,就不会触发唤醒;这个是每次阻塞轮询前判断,也就是有的任务添加进来,虽然不会立即唤醒阻塞轮询线程,但是当阻塞结束的时候,它一定就会跳出循环。结束,有新任务进来了。
      • 添加任务的时候,如果是false,会置为true。wakeup(boolean inEventLoop)。添加任务需要触发唤醒,需要满足4个条件。

EventLoopGroup和EventLoop

在放一张结论图:
在这里插入图片描述
到这边就彻底结束了。

结语

不知道有没有分享清楚我的学习过程以及EventLoop的运行机制?
不过我觉得还缺了一篇,结合IO模型分析EventLoop的使用场景,这样会更好的体会到EventLoop的强大以及加深对它的了解。我觉得我应该会写,但不会太快。

学习初衷很重要,在学习过程当中可以把握住重点,忽略掉一些干扰因素,提高自己的学习效率,但是也要记录下来哪些应该学习而没有学的。
学习方式也很重要,要知道自己怎么去学习,学习之前获取就要想明白一些。

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/103814483