翻阅源码时,我们会发现netty中很多方法的调用都是通过线程池的方式进行异步的调用,

这种  eventLoop.execute 方式的调用,实际上便是reactor线程。对应项目中使用广泛的NioEventLoop。还记得我们创建的两个reactor线程池么,具体代码可以参考 Netty源码 服务端的启动

首先来解释下什么事 reactor 线程

Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;
一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。

reactor线程的启动


io.netty.util.concurrent.SingleThreadEventExecutor#execute

reactor模型在第一次接受任务的时候,会启动线程

外部线程在提交任务时,netty会判断是否是当前前程是否是 SingleThreadEventExecutor中的Thread一致,如果不一致,说明需要启动一个新的线程接受任务。然后就会调用内部线程池执行reactor模型的run方法

然后就会执行addTask将线程封装成一个任务放到Queue中。Queue中的任务就是通过reactor线程来消费的。

reactor线程的执行


reactor线程做了三件事

1.不断的轮训注册到selector上channel的IO事件

2.处理IO事件,读取channel中的事件,选择一个work线程,准备执行任务

3.执行任务

上述三件事不断的轮训,下面我们依次进行分析。

1.select轮训

select轮训很简单,分为以下几步

1.延迟任务队列0.5秒以内有任务则中断

2.普通任务队列有任务添加则中断

3.阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有

  • 轮询到IO事件 (selectedKeys != 0
  • oldWakenUp 参数为true
  • 任务队列里面有任务(hasTasks
  • 第一个定时任务即将要被执行 (hasScheduledTasks()
  • 用户主动唤醒(wakenUp.get()

4.解决jdk空轮训bug,具体的bug我们可以看 https://www.jianshu.com/p/3ec120ca46b2

netty这边会记录每次轮训的时间,如果轮训的时间有效,累加器会加1,累加器到256之后,开始rebuildSelector,rebuildSelector的操作其实很简单:new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上

Select步骤结束,表示轮训到了io事件,那么接下来我们就要去处理这些事件

2.处理IO事件

这里出现了一个selectedKeys,selectedKeys的类型是SelectedSelectionKeySet,其实也就是一个Set集合

private SelectedSelectionKeySet selectedKeys;

这里的SelectionKey又是什么呢,我们可以看下类注释

A selection key is created each time a channel is registered with a selector

每一个channel在向selector注册时都会创建一个SelectionKey。

暂且我们先认为 SelectionKey里面包含了通道注册时的一些信息。

现在我们开始处理io事件

private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
// 1.取出IO事件以及对应的channel
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null;
final Object a = k.attachment();
// 2.处理该channel
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
// 3.判断是否该再来次轮询
if (needsToSelectAgain) {
for (;;) {
i++;
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
}
selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}

这里的k.attachment()能转换成AbstractNioChannel。

搜一下k.attach的调用关系,在io.netty.channel.nio.AbstractNioChannel#doRegister发现了如下代码,这能解释了selectionKey的创建

selectionKey = javaChannel().register(eventLoop().selector, 0, this);
public final SelectionKey register(Selector sel, int ops,
Object att)
throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (blocking)
throw new IllegalBlockingModeException();
SelectionKey k = findKey(sel);
if (k != null) {
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}

我们现在再来看processSelectedKey方法,代码精简后实际上就是调用unsafe对不同的事件进行处理

3.处理任务队列

netty中总共有三种任务类型

1.普通的eventLoop任务

channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
//task....
}
});

execute并没有真正去执行,而是将任务进行了封装。

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);
}
}

最终我们的任务添加到了一个任务队列中

protected void addTask(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
if (!offerTask(task)) {
reject(task);
}
} final boolean offerTask(Runnable task) {
if (isShutdown()) {
reject();
}
return taskQueue.offer(task);
}

这里 taskQueue并不是普通的任务队列,而是Mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便的将外部线程的task聚集,在reactor线程内部用单线程来串行执行

protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return PlatformDependent.newMpscQueue(maxPendingTasks);
}

2.外部任务

服务端在接收到客户端请求时,需要选择相应的channel写数据到客户端

ctx.channel().writeAndFlush(responsePacket);

这种在用户线程中的任务最终同样会被封装到任务队列,channel的write最终代码在io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, boolean, io.netty.channel.ChannelPromise)

private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}

先判断是否在eventloop线程,这里是false,最终封装成一个task,执行safeExecutor

private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) {
try {
executor.execute(runnable);
} catch (Throwable cause) {
try {
promise.setFailure(cause);
} finally {
if (msg != null) {
ReferenceCountUtil.release(msg);
}
}
}
}

接下来就和第一种情况一样,添加到队列当做。

3.定时任务

第三种场景就是定时任务逻辑,类似如下

eventLoop().schedule(new Runnable() {
@Override
public void run() { }, connectTimeoutMillis, TimeUnit.MILLISECONDS);

schedule的实际逻辑也是一个添加任务队列的过程

<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;
}

这里的scheduledTaskQueue是一个优先级队列,注意这里的线程安全问题,如果不是在eventloop线程提交的,那么就会把添加操作封装成一个task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对 PriorityQueue的访问就变成单线程,即只有reactor线程

Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
if (scheduledTaskQueue == null) {
scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
}
return scheduledTaskQueue;
}

现在再看runAllTasks方法

分为以下3步

  • 从scheduledTaskQueue转移定时任务到taskQueue
  • 计算本次任务循环的截止时间并执行
  • 执行完成任务后的任务

代码还是相当清晰的。这里不再深入。

最后我们再来总结下,reactor模型实质上就干了三件事情,首先他会不停的检测是否有io事件发生或者 是否有任务快要发生,如果检测到了,说明他要去干活了。首先先去处理io事件,所有的io事件都是通过unsafe去处理。处理完io事件后便开始处理任务队列里面的队列。

以上关于reactor模型的研究。

Netty源码 reactor 模型的更多相关文章

  1. Netty源码分析--Reactor模型(二)

    这一节和我一起开始正式的去研究Netty源码.在研究之前,我想先介绍一下Reactor模型. 我先分享两篇文献,大家可以自行下载学习.  链接:https://pan.baidu.com/s/1Uty ...

  2. Netty源码死磕一(netty线程模型及EventLoop机制)

    引言 好久没有写博客了,近期准备把Netty源码啃一遍.在这之前本想直接看源码,但是看到后面发现其实效率不高, 有些概念还是有必要回头再细啃的,特别是其线程模型以及EventLoop的概念. 当然在开 ...

  3. netty源码分析之揭开reactor线程的面纱(二)

    如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...

  4. Netty源码分析--内存模型(上)(十一)

    前两节我们分别看了FastThreadLocal和ThreadLocal的源码分析,并且在第八节的时候讲到了处理一个客户端的接入请求,一个客户端是接入进来的,是怎么注册到多路复用器上的.那么这一节我们 ...

  5. Netty源码阅读(一) ServerBootstrap启动

    Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...

  6. Netty源码—一、server启动(1)

    Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo.RocketMQ.可以说Netty是对Java NIO的封装,比如ByteBu ...

  7. EventLoop(netty源码死磕4)

    精进篇:netty源码  死磕4-EventLoop的鬼斧神工 目录 1. EventLoop的鬼斧神工 2. 初识 EventLoop 3. Reactor模式回顾 3.1. Reactor模式的组 ...

  8. Netty 源码(ChannelHandler 死磕)

    精进篇:netty源码死磕5  - 揭开 ChannelHandler 的神秘面纱 目录 1. 前言 2. Handler在经典Reactor中的角色 3. Handler在Netty中的坐标位置 4 ...

  9. ChannelHandler揭秘(Netty源码死磕5)

    精进篇:netty源码死磕5  揭开 ChannelHandler 的神秘面纱 目录 1. 前言 2. Handler在经典Reactor中的角色 3. Handler在Netty中的坐标位置 4. ...

随机推荐

  1. 头疼的Python 脚本报错

    Python 脚本报错 检查是否用了制表符.变量声明前面不能用制表符,只能用空格,版本为2.7.14

  2. vlc音视频开发(二)环境搭建(VS篇)

    来源:微信公众号「编程学习基地」 目录 简介 VS配置vlc开发环境 下载vlc源码 创建vlc环境 测试vlc代码 运行vlc程序 完成项目文件获取 简介 VLC 是一款自由.开源的跨平台多媒体播放 ...

  3. postcss 运用及原理

    postcss 入坑指南 目标: 掌握 postcss 的使用 自定义 postcss 插件 掌握 stylelint 的使用 自定义 stylelint rule 扩展 css parser 解释器 ...

  4. 深入了解typeof与instanceof的使用场景及注意事项

    JavaScript中的数据类型分为两类,undefined,number,boolean,string,symbol,bigint,null[1]组成的基础类型和Object.Function.Ar ...

  5. HDU - 4455 Substrings(非原创)

    XXX has an array of length n. XXX wants to know that, for a given w, what is the sum of the distinct ...

  6. 牛客多校第六场G Is Today Friday?(吉姆拉尔森/蔡勒公式 + 思维)题解

    题意: 给你\(A-J\)的字母组成的日期,形式为\(yyyy/mm/dd\).现给你\(n\)个这样的串\((n<=1e5)\),问你把字母映射成数字,并且使得所有日期合法且为星期五的最小字典 ...

  7. ysoserial Commons Collections2反序列化研究

    Apache Commons Collections2反序列化研究 环境准备 JDK 1.7 Commons Collections 4.0 javassit 前置知识 PriorityQueue() ...

  8. msfconsole web后门

    一.PHP后门 在meterpreter中有一个名为PHP meterpreter的payload,利用这个可以成功反弹.步骤如下: 1. 使用msfvenom 创建一个webshell.php 2. ...

  9. 将从摄像头即时读入的人像放入背景视频中_with_OpenCV_in_Python

    import cv2 import numpy as np import time cap = cv2.VideoCapture(0) background_capture = cv2.VideoCa ...

  10. ReactDOM API All In One

    ReactDOM API All In One React DOM API render() hydrate() unmountComponentAtNode() findDOMNode() crea ...