Nioeventloop原理

  • 1.说到nioEventloop(简称loop),其内部持有一个thread对象,而该loop的run方法正是由该线程执行的(我在历史文章中提高该loop如何被执行的)。
  • 2.除此之外loop还持有两个最重要的对象--selector和queue,前者就是我们的多路复用器后者则是存放我们的任务队列。
  • 3.selector:我们可以在selector上面注册我们的多个socketchannel,selector会给每个channel返回一个selectionKey,该selectionKey就是标识该channel注册在
    某一个selector上。
  • 4.我们一个socketchannel可以在一个selector注册多个事件因此会可能返回多个就绪事件,所以这也就是为啥我们在loop的run方法中需要每个事件都判断下。
  • 5.有的读者可能对于loop中的selector.selectNow(),selector.select(timeout),selector.select(),selector.wakeup()这四个方法理解不深刻,那么我们需要细说下
    selectNow方法代表获取已经就绪的selectionKey且该方法不会阻塞,如果没有就立马返回。而select(timeout)和select()与selectNow相似但是会阻塞,前者阻塞一段时间后者
    无限阻塞。
  • 6.那我们如何让selector从阻塞中恢复?对于select():当我们注册的channel有就绪的io事件或者当前执行的线程被interupt或者调用了wakeup方法,而对于select(timeout)
    则只是比select()多了一种恢复阻塞的方式就是到了指定的timeout时间。
  • 7.可以看到上述除了有就绪io事件或者等到指定超时时间我们恢复阻塞的被动方式,那么netty选择了哪种主动恢复被动方式?netty选择了wakreup。那netty为啥不选择线程中断方式?
    因为如果选择线程中断我们还需要抓捕该异常而采用wakeup方式只是让线程不会阻塞,并不会抛出异常。同时netty也不赞成我们在外部中断我们的loop执行线程,这会让netty以为 遇到了
    JDK selector的空轮询bug,进而导致不断重新构建selector。
  • 8.说到wakeup,我们需要强调下他是如何使用的。netty当发现任务队列里面没有任务要处理就会执行selector的阻塞方法select(timeout),如果这时候突然有外部线程调用channel去发送一个消息,
    那么netty会把改消息包装成一个任务放入队列,放入队列的时候netty会检测当前selector是否处于阻塞,如果是则调用wakeup方法让selector从阻塞中恢复过来进行loop的run方法执行。
  • 9.还有一个需要注意的地方就是当我们先调用selector.wakeup方法,下一步执行selector阻塞方法就会立马返回不会阻塞。但是如果我们在调用阻塞方法前调用了selectNow这个方法会清除wakeup的影响,从而我们下面再调用阻塞方法也不会被唤醒,除非我们在调用一次wakeup方法。
  • 10.下面我们说下loop里面另外一个重要对象queue-MpscChunkedArrayQueue,该队列是支持多生产单消费者的。该模型正好符合netty的线程执行模型,即可以有多个外部线程将任务提交给loop而loop会在每次循环的时候进行处理。那么MpscChunkedArrayQueue的优点是啥。
  • 11.首先其取消了锁,通过自旋+cas来将任务存放到队列中,当然如果并发频率太高也会使得我们的cpu使用率比较高,所以建议使用时将这些线程绑定到特定的CPU;
  • 12.其次消除了伪共享,我们都知道我们cpu读取主内存的数据需要经过三个cpu cache,而每个cpu cache的最小单位都是一个cache line,当我们改变cacheline上某一个内存值会导致整个cacheline不可用,从而需要从主内存获取最新数据,而我们队列中的数据都是在数组中的,所以很有可能我们改变了队列里面第一个对象值,导致后续的队列值在cache line中不可用需要重新load。所以MpscChunkedArrayQueue通过了。
    CacheLine Padding,是的我们每一个队列数据占据一个完全的cach line 这样他的修改不会影响其他队列的在cache line的数据。
  • 13.最后可以动态扩容,虽然我们这边是数组,但是它的动态扩容并不是把新数据和老数据放到一个新的大数组,而在new一个新的数组存放新的队列数据,然后老的数组最后一个存放一个指针指向新new的数组。
  • 14.上面说完loop大致涉及到的内容,我们现在说下loop工作的逻辑即其run方法,具体如下:
  • 15.首先判断任务队列是否有值,如果有值直接调用selectorNow,这边调用完selectorNow,还需要判断wakeup是否等于true,因为如果等于则代表我们之前调用了selector.wakeUP方法而该方法被selectorNow取消了,所以需要重新调用。
    此外如何没有值则执行select( oldWakenUp)操作。
  • 16.select( oldWakenUp)操作的主要逻辑就是先设置wakeup=false这就是代表了在此期间外界不可以调用selector的wakeup方法打断该方法的逻辑,该方法主要就时轮训获取io时间,而轮训的时间就是到下一个定时任务截止的时间。
  • 17.在上述方法中还会去判断是否触发了jdk的selector的空轮旬bug,主要原理就是发现每次selector从阻塞方法中醒来既没有任务也没有io时间且阻塞的时间也没到我们设置的时间,如果一直重复这样到达一定的阈值,则我们就rebuildselector。
  • 18.当然了netty在上面也会判断下释放因为触发线程的中断方法导致的,如果是则打印异常日志从而提醒使用者避免触发loop线程的中断。
  • 19.当上面获取完成了之后开始根据ioRatio来执行io就绪事件和task,这个ioRatio可以限制task的执行事件,但是只能是大致限制比如我们限制3秒钟那么我们执行到第二个任务才2秒那netty会继续执行,但是第二个任务耗时4秒则其一共执行的时间就会超过之前的3秒。
  • 20.processSelectedKeys:该方法就是执行就绪事件,netty还做了一个优化就是通过反射把Selector的实现类中两个属性selectedKeys(就绪key集合)和publicSelectedKeys(给外部访问就绪io事件的集合)从hashSet替换数组。
  • 21.那么netty为啥要把hashSet替换为数组?首先当netty发现有io事件就绪就会把selectionKey塞入上述set集合,如果采用set,那么当io事件变多导致hash冲突进而形成链表,那么我们add操作就不是常数复杂度
  • 22.如果采用数组我们不仅省去了计算hash的事件也不会存在因为要把key插入链表需要迭代,直接插入数组尾部即可。插入操作的复杂度永远是常数范围。
  • 22.处理io事件的时候会根据io事件类型类执行aceept,read,conncect和write操作。先完成connect操作一般有connect事件则代表是客户端还未完成链接,需要先处理完connect,然后在判断是否有write事件,如果有write事件代表上次该netty发送消息并没有全部发送完成(因为底层的channel已经满了)而此时channel有空闲buffer 可以发送数据。下面就是处理read和accept事件。
  • 23.runAllTasks执行我们的task,我们的task其实真正的有三个队列分别是定时任务队列(scheduledTaskQueue),常规任务队列(taskQueue),还有就是后置队列(tailTasks)
  • 24.scheduledTaskQueue是优先队列会按照快要到时的时间排队列,所以每次比较队列的队首如果已经过期或者正好到时见就从时间队列剔除放入带taskQueue,r如果放入失败还是继续放回时间队列,然后开始执行taskQueque的队列,
    每次执行完成任务看看我们设置的任务调度时间是否超过如果超过了就直接返回,而比较的方式就是采用lastExecutionTime与我们的deadline进行比较,lastExecutionTime会在taskqueue执行完毕或者执行了64次之后在更新,而且每次也是64次执行完任务在比较。
    原因就是获取nanoTime是需要系统调用,比较损耗性能。
  • 25.当我们执行完成上述的2个队列的任务,会最终快要结束的执行tailTask的任务队列。我们可以通过nioEventLoop.executeAfterEventLoopIteration()来添加任务到tailTask,不过netty不建议,因为该方法有可能导致我们的loop获取io事件更快速。
  • 26.至此我们nioEventloop讲解结束了!!

你可能感兴趣的:(Nioeventloop原理)