Netty源码 reactor 模型
翻阅源码时,我们会发现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 模型的更多相关文章
- Netty源码分析--Reactor模型(二)
这一节和我一起开始正式的去研究Netty源码.在研究之前,我想先介绍一下Reactor模型. 我先分享两篇文献,大家可以自行下载学习. 链接:https://pan.baidu.com/s/1Uty ...
- Netty源码死磕一(netty线程模型及EventLoop机制)
引言 好久没有写博客了,近期准备把Netty源码啃一遍.在这之前本想直接看源码,但是看到后面发现其实效率不高, 有些概念还是有必要回头再细啃的,特别是其线程模型以及EventLoop的概念. 当然在开 ...
- netty源码分析之揭开reactor线程的面纱(二)
如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...
- Netty源码分析--内存模型(上)(十一)
前两节我们分别看了FastThreadLocal和ThreadLocal的源码分析,并且在第八节的时候讲到了处理一个客户端的接入请求,一个客户端是接入进来的,是怎么注册到多路复用器上的.那么这一节我们 ...
- Netty源码阅读(一) ServerBootstrap启动
Netty源码阅读(一) ServerBootstrap启动 转自我的Github Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...
- Netty源码—一、server启动(1)
Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo.RocketMQ.可以说Netty是对Java NIO的封装,比如ByteBu ...
- EventLoop(netty源码死磕4)
精进篇:netty源码 死磕4-EventLoop的鬼斧神工 目录 1. EventLoop的鬼斧神工 2. 初识 EventLoop 3. Reactor模式回顾 3.1. Reactor模式的组 ...
- Netty 源码(ChannelHandler 死磕)
精进篇:netty源码死磕5 - 揭开 ChannelHandler 的神秘面纱 目录 1. 前言 2. Handler在经典Reactor中的角色 3. Handler在Netty中的坐标位置 4 ...
- ChannelHandler揭秘(Netty源码死磕5)
精进篇:netty源码死磕5 揭开 ChannelHandler 的神秘面纱 目录 1. 前言 2. Handler在经典Reactor中的角色 3. Handler在Netty中的坐标位置 4. ...
随机推荐
- java通过HttpClient调用接口总结
2.HttpClient 2.1简介: 最近看项目的代码,看到工程中有两个jar包张的很像,一个是commons.httpclient-3.1.jar,一个是httpclient4.2.1.jar,很 ...
- 【论文笔记】AutoML for MCA on Mobile Devices——论文解读与代码解析
理论部分 方法介绍 本节将详细介绍AMC的算法流程.AMC旨在自动地找出每层的冗余参数. AMC训练一个强化学习的策略,对每个卷积层会给出其action(即压缩率),然后根据压缩率进行裁枝.裁枝后,A ...
- kubeadm---高可用安装
1.修改主机名 如何使用hostnamectl set-hostname name来为每台主机设置不同的机器名 #hostnamectl set-hostname k8s-master01 或者使用以 ...
- ArcGIS处理栅格数据(二)
五.栅格数据的配准 1.有参考图层 ① 在ArcMap里面添加需要配准的栅格数据集和参考数据集. ② 在ArcMap里面添加"地理配准"工具条. 添加成功后如下图所示: ③ 将需要 ...
- Linux系统诊断必备技能之二:日志查询常用命令详解
一.概述 日常运维工作中,排查线上环境问题,少不了去线上查日志.而使用什么命令,能快速准确地查到我们需要查找地日志信息,也是我们需要掌握的一项技能.下面介绍一下日常工作常用到的查看日志命令:tail, ...
- Leetcode(7)-反转整数
给定一个 32 位有符号整数,将整数中的数字进行反转. 示例 1: 输入: 123 输出: 321 示例 2: 输入: -123 输出: -321 示例 3: 输入: 120 输出: 21 注意: 假 ...
- 你不知道的 JS (系列丛书) - 第二版
你不知道的 JS (系列丛书) - 第二版 You Don't Know JS (book series) - 2nd Edition https://github.com/learning-js-b ...
- WebSocket All In One
WebSocket All In One WebSocket heartbeat WebSocket 心跳检测 ping pong refs xgqfrms 2012-2020 www.cnblogs ...
- jsbridge 原理 & 通信原理
jsbridge 原理 & 通信原理 Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中; 因此,Hybrid 方案中 JSB ...
- 未来,Baccarat将如何拓展生态版图?
Baccarat最近几度冲上DeFi版面的热搜,一是因为BGV价格不断的上涨,二是因为生态建设者的不断涌入.可以说,当下的Baccarat,实在是太火爆了.那么在未来,Baccarat还将会持续火爆吗 ...