Netty 源码 NioEventLoop(三)执行流程

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

相关文章:

上文提到在启动 NioEventLoop 线程时会执行 SingleThreadEventExecutor#doStartThread(),在这个方法中调用 SingleThreadEventExecutor.this.run(),NioEventLoop 重写了 run() 方法。NioEventLoop#run() 代码如下:

@Override
protected void run() {
for (;;) {
try {
// 1. select 策略选择
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
// 1.1 非阻塞的 select 策略。实际上,默认情况下,不会返回 CONTINUE 的策略
case SelectStrategy.CONTINUE:
continue;
// 1.2 阻塞的 select 策略
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
// 1.3 不需要 select,目前已经有可以执行的任务了
default:
} // 2. 执行网络 IO 事件和任务调度
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
// 2.1. 处理网络 IO 事件
processSelectedKeys();
} finally {
// 2.2. 处理系统 Task 和自定义 Task
runAllTasks();
}
} else {
// 根据 ioRatio 计算非 IO 最多执行的时间
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// 3. 关闭线程
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}

NioEventLoop#run() 做了记下事情:

  1. 根据 selectStrategy 执行不同的策略
  2. 执行网络 IO 事件和任务调度
  3. 关闭线程

1. IO 轮询策略

当 taskQueue 中没有任务时,那么 Netty 可以阻塞地等待 IO 就绪事件。而当 taskQueue 中有任务时,我们自然地希望所提交的任务可以尽快地执行 ,因此 Netty 会调用非阻塞的 selectNow() 方法,以保证 taskQueue 中的任务尽快可以执行。

(1) hasTasks

首先,在 run 方法中,第一步是调用 hasTasks() 方法来判断当前任务队列中是否有任务

protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty();
}

这个方法很简单,仅仅是检查了一下 taskQueue 是否为空。至于 taskQueue 是什么呢,其实它就是存放一系列的需要由此 EventLoop 所执行的任务列表。关于 taskQueue,我们这里暂时不表,等到后面再来详细分析它。

(2) DefaultSelectStrategy

// NioEventLoop#selectNowSupplier
private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
}; // 非阻塞的 select 策略。实际上,默认情况下,不会返回 CONTINUE 的策略
SelectStrategy.SELECT = -1;
// 阻塞的 select 策略
SelectStrategy.CONTINUE = -2; // DefaultSelectStrategy
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}

显然当 taskQueue 为空时,执行的是 select(oldWakenUp) 方法。那么 selectNow() 和 select(oldWakenUp) 之间有什么区别呢? 来看一下,selectNow() 的源码如下

(3) selectNow

int selectNow() throws IOException {
try {
return selector.selectNow();
} finally {
// restore wakeup state if needed
if (wakenUp.get()) {
selector.wakeup();
}
}
}

调用 JDK 底层的 selector.selectNow()。selectNow() 方法会检查当前是否有就绪的 IO 事件,如果有,则返回就绪 IO 事件的个数;如果没有,则返回 0。注意,selectNow() 是立即返回的,不会阻塞当前线程。当 selectNow() 调用后,finally 语句块中会检查 wakenUp 变量是否为 true,当为 true 时,调用 selector.wakeup() 唤醒 select() 的阻塞调用。

(4) select(boolean oldWakenUp)

private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectedKeys = selector.select(timeoutMillis);
} catch (CancelledKeyException e) {
}
}

在这个 select 方法中,调用了 selector.select(timeoutMillis),而这个调用是会阻塞住当前线程的,timeoutMillis是阻塞的超时时间。到来这里,我们可以看到,当 hasTasks() 为真时,调用的的 selectNow() 方法是不会阻塞当前线程的,而当 hasTasks() 为假时,调用的 select(oldWakenUp) 是会阻塞当前线程的。

2. IO 事件的处理

在 NioEventLoop.run() 方法中,第一步是通过 select/selectNow 调用查询当前是否有就绪的 IO 事件,那么当有 IO 事件就绪时,第二步自然就是处理这些 IO 事件啦。首先让我们来看一下 NioEventLoop.run 中循环的剩余部分:

final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
// 2.1. 处理网络 IO 事件
processSelectedKeys();
} finally {
// 2.2. 处理系统 Task 和自定义 Task
runAllTasks();
}
} else {
// 根据 ioRatio 计算非 IO 最多执行的时间
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}

上面列出的代码中,有两个关键的调用:

  • 第一个是 processSelectedKeys():处理准备就绪的 IO 事件;
  • 第二个是 runAllTasks():运行 taskQueue 中的任务。

这里的代码还有一个十分有意思的地方,即 ioRatio。那什么是 ioRatio 呢?它表示的是此线程分配给 IO 操作所占的时间比(即运行 processSelectedKeys 耗时在整个循环中所占用的时间)。例如 ioRatio 默认是 50,则表示 IO 操作和执行 task 的所占用的线程执行时间比是 1 : 1。当知道了 IO 操作耗时和它所占用的时间比,那么执行 task 的时间就可以很方便的计算出来了。

我们这里先分析一下 processSelectedKeys() 方法调用,runAllTasks() 留到下面再分析。processSelectedKeys() 方法的源码如下:

private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

由于默认未开启 selectedKeys 优化功能,所以会进入 processSelectedKeysPlain 分支执。下面继续分析 processSelectedKeysPlain 的代码实现。

private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
// https://github.com/netty/netty/issues/597
if (selectedKeys.isEmpty()) {
return;
} Iterator<SelectionKey> i = selectedKeys.iterator();
for (;;) {
final SelectionKey k = i.next();
final Object a = k.attachment();
i.remove(); if (a instanceof AbstractNioChannel) {
// NioSocketChannel 或 NioServerSocketChannel 进行 IO 读写相关的操作
processSelectedKey(k, (AbstractNioChannel) a);
} else {
// 用户自行注册的 Task 任务,一般情况下不会执行
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
} if (!i.hasNext()) {
break;
}
// 省略...
}
}

processSelectedKey 方法源码如下:

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
// 省略... try {
int readyOps = k.readyOps();
// 1. OP_CONNECT 读写前要先处理连接,否则可能抛 NotYetConnectedException 异常
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops); unsafe.finishConnect();
} // 2. OP_WRITE
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
} // 3. OP_READ 或 OP_ACCEPT
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

这个代码是不是很熟悉啊?完全是 Java NIO 的 Selector 的那一套处理流程嘛!processSelectedKey 中处理了三个

事件,分别是:

  • OP_READ 可读事件,即 Channel 中收到了新数据可供上层读取.
  • OP_WRITE 可写事件,即上层可以向 Channel 写入数据.
  • OP_CONNECT 连接建立事件,即 TCP 连接已经建立,Channel 处于 active 状态.

下面我们分别根据这三个事件来看一下 Netty 是怎么处理的吧。

2.1 OP_READ

当就绪的 IO 事件是 OP_READ,代码会调用 unsafe.read() 方法。unsafe 我们已见过多次,NioSocketChannel 的 Unsafe 是在 AbstractNioByteChannel 中实现的,而 NioServerSocketChannel 的 Unsafe 是在 NioMessageUnsafe 中实现。

public final void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config); ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// 1. 分配缓冲区 ByteBuf
byteBuf = allocHandle.allocate(allocator);
allocHandle.lastBytesRead(doReadBytes(byteBuf));
// 2. 从 NioSocketChannel 中读取数据
if (allocHandle.lastBytesRead() <= 0) {
// nothing was read. release the buffer.
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
} allocHandle.incMessagesRead(1);
readPending = false;
// 3. 调用 pipeline.fireChannelRead 发送一个 inbound 事件
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading()); allocHandle.readComplete();
pipeline.fireChannelReadComplete(); if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}

上面 read 方法其实归纳起来,可以认为做了如下工作:

  1. 分配 ByteBuf
  2. 从 SocketChannel 中读取数据
  3. 调用 pipeline.fireChannelRead 发送一个 inbound 事件

2.2 OP_WRITE

OP_WRITE 可写事件代码如下。这里代码比较简单,没有详细分析的必要了。

if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}

2.3 OP_CONNECT

最后一个事件是 OP_CONNECT,即 TCP 连接已建立事

if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// 已连接后就需要注销 OP_CONNECT 事件 See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops); unsafe.finishConnect();
}

OP_CONNECT 事件的处理中,只做了两件事情:

  1. 正如代码中的注释所言, 我们需要将 OP_CONNECT 从就绪事件集中清除, 不然会一直有 OP_CONNECT 事件。

  2. 调用 unsafe.finishConnect() 通知上层连接已建立

    unsafe.finishConnect() 调用最后会调用到 pipeline().fireChannelActive(),产生一个 inbound 事件,通知 pipeline 中的各个 handler TCP 通道已建立(即 ChannelInboundHandler.channelActive 方法会被调用)

到了这里,我们整个 NioEventLoop 的 IO 操作部分已经了解完了,接下来的一节我们要重点分析一下 Netty 的任务

队列机制。

3. 任务调度

我们已经提到过,在 Netty 中,一个 NioEventLoop 通常需要肩负起两种任务,第一个是作为 IO 线程,处理 IO 操作;第二个就是作为任务线程,处理 taskQueue 中的任务。这一节的重点就是分析一下 NioEventLoop 的任务队列机制

的。

3.1 普通 Runnable 任务

// SingleThreadEventExecutor
private final Queue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>(maxPendingTasks);
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 是存放着待执行的任务的队列。

3.2 schedule 任务

除了通过 execute 添加普通的 Runnable 任务外,我们还可以通过调用 eventLoop.scheduleXXX 之类的方法来添加

一个定时任务。schedule 功能的实现是在 SingleThreadEventExecutor 的父类,即 AbstractScheduledEventExecutor 中实现的。

// SingleThreadEventExecutor
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;

scheduledTaskQueue 是一个队列(Queue),其中存放的元素是 ScheduledFutureTask。而ScheduledFutureTask 我们很容易猜到,它是对 Schedule 任务的一个抽象。我们来看一下 AbstractScheduledEventExecutor 所实现的 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;
}

3.3 执行调度任务

protected boolean runAllTasks() {
assert inEventLoop();
boolean fetchedAll;
boolean ranAtLeastOne = false; do {
fetchedAll = fetchFromScheduledTaskQueue();
if (runAllTasksFrom(taskQueue)) {
ranAtLeastOne = true;
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks. if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
afterRunningAllTasks();
return ranAtLeastOne;
}

我们前面已经提到过,EventLoop 可以通过调用 EventLoop.execute 来将一个 Runnable 提交到 taskQueue 中,

也可以通过调用 EventLoop.schedule 来提交一个 schedule 任务到 scheduledTaskQueue 中。在此方法的一开

始调用的 fetchFromScheduledTaskQueue() 其实就是将 scheduledTaskQueue 中已经可以执行的(即定时时

间已到的 schedule 任务) 拿出来并添加到 taskQueue 中,作为可执行的 task 等待被调度执行。代码如下:

private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) {
if (!taskQueue.offer(scheduledTask)) {
// No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
scheduledTask = pollScheduledTask(nanoTime);
}
return true;
}

接下来 runAllTasks() 方法就会不断调用 task = pollTask() 从 taskQueue 中获取一个可执行的 task,然后调用它

的 run() 方法来运行此 task。

注意: 因为 EventLoop 既需要执行 IO 操作,又需要执行 task,因此我们在调用 EventLoop.execute 方法提交

任务时,不要提交耗时任务,更不能提交一些会造成阻塞的任务,不然会导致我们的 IO 线程得不到调度,影响整

个程序的并发量。


每天用心记录一点点。内容也许不重要,但习惯很重要!

Netty 源码 NioEventLoop(三)执行流程的更多相关文章

  1. Netty 源码 NioEventLoop(一)初始化

    Netty 源码 NioEventLoop(一)初始化 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) Netty 基于事件 ...

  2. SpringMVC源码剖析1——执行流程

    SpringMVC源码剖析1——执行流程 00.SpringMVC执行流程file:///C:/Users/WANGGA~1/AppData/Local/Temp/enhtmlclip/Image.p ...

  3. 【原创】angularjs1.3.0源码解析之执行流程

    Angular执行流程 前言 发现最近angularjs在我厂的应用变得很广泛,下周刚好也有个angular项目要着手开始做,所以先做了下功课,从源代码开始入手会更深刻点,可能讲的没那么细,侧重点在于 ...

  4. Django drf:序列化增删改查、局部与全局钩子源码流程、认证源码分析、执行流程

    一.序列化类的增.删.改.查 用drf的序列化组件   -定义一个类继承class BookSerializer(serializers.Serializer):   -写字段,如果不指定source ...

  5. [旧][Android] Retrofit 源码分析之执行流程

    备注 原发表于2016.04.23,资料已过时,仅作备份,谨慎参考 前言 由于是第一次自己翻看源代码进行学习,加上基础不好,在看源代码的过程中简直痛苦不堪,但同时也暴露出了自己的许多问题.我觉得学习源 ...

  6. Netty 源码学习——服务端流程分析

    在上一篇我们已经介绍了客户端的流程分析,我们已经对启动已经大体上有了一定的认识,现在我们继续看对服务端的流程来看一看到底有什么区别. 服务端代码 public class NioServer { pr ...

  7. mybatis(五):源码分析 - sqlsession执行流程

  8. Netty 源码(二)NioEventLoop 之 Channel 注册

    Netty 源码(二)NioEventLoop 之 Channel 注册 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一 ...

  9. Netty源码分析之NioEventLoop(三)—NioEventLoop的执行

    前面两篇文章Netty源码分析之NioEventLoop(一)—NioEventLoop的创建与Netty源码分析之NioEventLoop(二)—NioEventLoop的启动中我们对NioEven ...

随机推荐

  1. java.net.NoRouteToHostException

    1.之前一直默认的一个请求url,后来后台ip更换后,就报上述错误.网上好多方法说:关闭服务器端的防火墙,但试过没用. 问题有待重新测试解决

  2. HashMap、LinkedHashMap、ConcurrentHashMap、ArrayList、LinkedList 底层实现

    HashMap相关问题 1.你用过HashMap吗?什么是HashMap?你为什么用到它? 用过,HashMap是基于哈希表的Map接口的非同步实现,它允许null键和null值,且HashMap依托 ...

  3. Windows消息循环

    首先理解一句话:“Windows”向应用程序发送了一条消息.这里是指Windows调用了该程序内部的一个函数. 当UpdateWindow被调用后,新建的窗口在屏幕便完全可见了.此时,Windows会 ...

  4. Android笔记:Button

    示例代码摘自<第一行代码> ButtonDemo.java的代码: public class ButtonDemo extends Activity { @Override protect ...

  5. nginx 刷新显示404

    HTML5 History 模式 vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载. 如果不想要很丑的 ...

  6. 前端框架(kraken、Express、Node、MVC)

    You know my loneliness is only kept for you, my sweet songs are only sang for you. 前端框架相关知识记录. krake ...

  7. string类的一些函数方法

    1.请查看String.equals()方法的实现代码,注意学习其实现方法: (1)源程序: public class StringEquals { /** * @param args the com ...

  8. Django使用jsonp和cors解决跨域请求问题

    1.使用jsonp的方式解决跨域请求的问题 我启动两个django项目,然后使用的端口不一样,在项目1中通过ajax发请求给项目2,然后接受项目2发送过来的数据 先看项目1的ajax的代码 $(&qu ...

  9. unbutu中安装jdk并编写第一个java程序

    第一部分:安装jdk 1.首先在putty控制台中输入如下命令,检验系统中是否已经装有jdk. java -version 如果显式的结果如下图,则说明没有安装. openjdk是在linux下默认安 ...

  10. Mysql在Linux的基本操作文档

    总结了Mysql在Linux下的应用,以下是Linux操作系统操作MySQL常用命令小结,需要的朋友参考下: 1.Mysql服务 # chkconfig --list 列出所有系统服务 # chkco ...