翻阅源码时,我们会发现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. 【转】REST风格框架实战:从MVC到前后端分离(附完整Demo)

    版权声明:欢迎转载,注明作者和出处就好!如果不喜欢或文章存在明显的谬误,请留言说明原因再踩哦,谢谢,我也可以知道原因,不断进步! https://blog.csdn.net/justloveyou_/ ...

  2. 进程控制——fork-and-exec、system、wait

    forc-and-exec流程 父进程与子进程之间的关系十分复杂,最大的复杂点在于进程间相互调用.Linux下这一流程称为fork-and-exec.父进程通过fork的方式产生一个一模一样的子进程, ...

  3. msf 信息收集

    MSF信息收集 转载自天堂空气 一 MSF主机发现 0x1:搜索arp 0x2:使用use auxiliary/scanner/discovery/arp_sweep 模块,然后show option ...

  4. 对于maven中无法加载类路径下的配置文件

    <build> <resources> <resource> <directory>src/main/java</directory> &l ...

  5. 5分钟学Go 基础01:初识 Go 的第一印象是薪水可观

    本文首发于公众号「5分钟学Go」,一个让你每次花 5 分钟就能掌握一个技能点的公众号.目前在博主连更 5 分钟学Go系列,大家可以关注下,第一时间掌握Go技能.如果想要加群交流,可以在公众号后台回复「 ...

  6. VSCode 在 Vue 导入路径中使用 @ 符号后无法正确跳转 bug

    VSCode 在 Vue 导入路径中使用 @ 符号后无法正确跳转 bug bug jsconfig.json { // This file is required for VSCode to unde ...

  7. API 授权 All In One

    API 授权 All In One 身份验证 授权类型 身份验证类型 继承认证 没有认证 API密钥 不记名令牌 基本认证 摘要授权 OAuth 1.0 OAuth 2.0 授权码 隐含的 密码凭证 ...

  8. taro H5

    taro H5 开发指南 https://nervjs.github.io/taro/docs/GETTING-STARTED.html#h5 taro # build $ taro build -- ...

  9. js 最简单的发布订阅模式

    let _subscriber: any; function autorun(subscriber: Function) { _subscriber = subscriber; _subscriber ...

  10. 伦尼斯酒庄(Chateau Renice)再次赞助亚洲50大餐厅赛事

    连续几年来,伦尼斯酒庄(Chateau Renice)一直是亚洲50大最佳餐厅评选赛(Asia's 50 Best Restaurant Awards)的赞助商.2020年伦尼斯酒庄酒庄(Chatea ...