翻阅源码时,我们会发现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. k8s二进制部署 - node节点安装

    创建kubelet配置 • set-cluster # 创建需要连接的集群信息,可以创建多个k8s集群信息 [root@hdss7-21 ~]# kubectl config set-cluster ...

  2. Chapter Zero 0.1.2 CPU的架构

    CPU的架构 CPU内部含有一些微指令, 我们所使用的软件都要经过CPU内部的微指令集达成才行. 这些指令集的设计又分为两种设计理念, 这就是目前世界上常见的两种主要CPU架构: 精简指令集(Redu ...

  3. Python 3的f-Strings:增强的字符串格式语法(指南)

    最近也在一个视频网站的爬虫,项目已经完成,中间有不少需要总结的经验. 从Python 3.6开始,f-Strings是格式化字符串的一种很棒的新方法.与其他格式化方式相比,它们不仅更具可读性,更简洁且 ...

  4. 一些CTF题目--20/9/3

    1. 看源码 POST方法.Extract覆盖. 直接url ?参数不行,因为POST参数不在URL上,GET参数才在 Burpsuite抓包,改成  pass=1&thepassword_1 ...

  5. 【非原创】codeforces 1070C Cloud Computing 【线段树&树状数组】

    题目:戳这里 学习博客:戳这里 题意:有很多个活动,每个活动有持续天数,每个活动会在每天提供C个CPU每个CPU价格为P,问需要工作N天,每天需要K个CPU的最少花费. 解题思路:遍历每一天,维护当前 ...

  6. SpringBoot 全局视角看springboot

    从单体架构到微服务 单体架构 任何一个网站在发布初期几乎都不可能立马就拥有庞大的用户流量和海量数据,都是在不停 的试错过程中一步一步演变其自身架构,满足其自身业务.比如现在能够抗住双十一这么大 流量的 ...

  7. 重新上手c语言的一些坑

    c语言结构体不能声明函数,放几个函数指针倒是没问题 c语言结构体不能在声明时初始化 声明两个指针 int *a,*b; 或者typedef int* int_P int_P a,b; typedef要 ...

  8. Ubuntu 18.04 + pip3 install virtualenvwrapper 报错 ERROR: virtualenvwrapper could not find virtualenv in your path

    接上片... 问题 virtualenvwrapper装好后, 发现使用mkvirtualenv XX时, 又找不到virtualenv了... apt install python3-virtual ...

  9. Adaptive Threshold

    Adaptive Threshold 1. Otsu's Binarization: Using a discriminant analysis to partition the image into ...

  10. Android vs iOS vs Web

    Android vs iOS vs Web UI view Android ViewGroup ImageView TextView iOS UIView ImageView TextView Web ...