Netty源码分析第二章: NioEventLoop

 

第六节: 执行select操作

分析完了selector的创建和优化的过程, 这一小节分析select相关操作

跟到跟到select操作的入口,NioEventLoop的run方法:

protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//轮询io事件(1)
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
//默认是50
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
runAllTasks();
}
} else {
//记录下开始时间
final long ioStartTime = System.nanoTime();
try {
//处理轮询到的key(2)
processSelectedKeys();
} finally {
//计算耗时
final long ioTime = System.nanoTime() - ioStartTime;
//执行task(3)
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
//代码省略
}
}

代码比较长, 其实主要分为三部分:

1. 轮询io事件

2. 处理轮询到的key

3. 执行task

这一小节, 主要剖析第一部分, 轮询io事件

首先switch块中默认会走到SelectStrategy.SELECT中, 执行select(wakenUp.getAndSet(false))方法

参数wakenUp.getAndSet(false)代表当前select操作是未唤醒状态

进入到select(wakenUp.getAndSet(false))方法中:

private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
//当前系统的纳秒数
long currentTimeNanos = System.nanoTime();
//截止时间=当前时间+队列第一个任务剩余执行时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
//阻塞时间(毫秒)=(截止时间-当前时间+0.5毫秒)
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
//进行阻塞式的select操作
int selectedKeys = selector.select(timeoutMillis);
//轮询次数
selectCnt ++;
//如果轮询到一个事件(selectedKeys != 0), 或者当前select操作需要唤醒(oldWakenUp),
//或者在执行select操作时已经被外部线程唤醒(wakenUp.get()),
//或者任务队列已经有任务(hasTask), 或者定时任务队列中有任务(hasScheduledTasks())
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
//省略
//记录下当前时间
long time = System.nanoTime();
//当前时间-开始时间>=超时时间(条件成立, 执行过一次select操作, 条件不成立, 有可能发生空轮询)
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
//代表已经进行了一次阻塞式select操作, 操作次数重置为1
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//省略日志代码
//如果空轮询的次数大于一个阈值(512), 解决空轮询的bug
rebuildSelector();
selector = this.selector;
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
//代码省略
} catch (CancelledKeyException e) {
//省略
}
}

首先通过 long currentTimeNanos = System.nanoTime() 获取系统的纳秒数

继续往下看:

long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

delayNanos(currentTimeNanos)代表距定时任务中第一个任务剩余多长时间, 这个时间+当前时间代表这次操作不能超过的时间, 因为超过之后定时任务不能严格按照预定时间执行, 其中定时任务队列是已经按照执行时间有小到大排列好的队列, 所以第一个任务则是最近需要执行的任务, selectDeadLineNanos就代表了当前操作不能超过的时间

然后就进入到了无限for循环

for循环中我们关注:

long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L

selectDeadLineNanos - currentTimeNanos+500000L 代表截止时间-当前时间+0.5毫秒的调整时间, 除以1000000表示将计算的时间转化为毫秒数

最后算出的时间就是selector操作的阻塞时间, 并赋值到局部变量的timeoutMillis中

后面有个判断 if(imeoutMillis<0) , 代表当前时间已经超过了最后截止时间+0.5毫秒,  selectCnt == 0 代表没有进行select操作, 满足这两个条件, 则执行selectNow()之后, 将selectCnt赋值为1之后跳出循环

如果没超过截止时间, 就进行了 if(hasTasks() && wakenUp.compareAndSet(false, true)) 判断

这里我们关注hasTasks()方法, 这里是判断当前NioEventLoop所绑定的taskQueue是否有任务, 如果有任务, 则执行selectNow()之后, 将selectCnt赋值为1之后跳出循环(跳出循环之后去执行任务队列中的任务)

hasTasks()方法可以自己跟一下, 非常简单

如果没有满足上述条件, 就会执行 int selectedKeys = selector.select(timeoutMillis) 进行阻塞式轮询, 并且自增轮询次数, 而后会进行如下判断:

if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}

selectedKeys != 0代表已经有轮询到的事件, oldWakenUp代表当前select操作是否需要唤醒, wakenUp.get()说明已经被外部线程唤醒, hasTasks()代表任务队列是否有任务, hasScheduledTasks()代表定时任务队列是否任务, 满足条件之一, 就跳出循环

long time = System.nanoTime() 记录了当前的时间, 之后有个判断:

if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) 这里的意思是当前时间-阻塞时间>方法开始执行的时间, 这里说明已经完整的执行完成了一个阻塞的select()操作, 将selectCnt设置成1

如果此条件不成立, 说明没有完整执行select()操作, 可能触发了一次空轮询, 根据前一个selectCnt++这步我们知道, 每触发一次空轮询selectCnt都会自增

之后会进入第二个判断 SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD

其中SELECTOR_AUTO_REBUILD_THRESHOLD默认是512, 这个判断意思就是空轮询的次数如果超过512次, 则会认为是发生了epoll bug, 这样会通过rebuildSelector()方法重新构建selector, 然后将重新构建的selector赋值到局部变量selector, 执行一次selectNow(), 将selectCnt初始化1, 跳出循环

rebuildSelector()方法中, 看netty是如何解决epoll bug的:

public void rebuildSelector() {
//是否是由其他线程发起的
if (!inEventLoop()) {
//如果是其他线程发起的, 将rebuildSelector()封装成任务队列, 由NioEventLoop进行调用
execute(new Runnable() {
@Override
public void run() {
rebuildSelector();
}
});
return;
}
final Selector oldSelector = selector;
final Selector newSelector;
if (oldSelector == null) {
return;
}
try {
//重新创建一个select
newSelector = openSelector();
} catch (Exception e) {
logger.warn("Failed to create a new Selector.", e);
return;
}
int nChannels = 0;
for (;;) {
try {
//拿到旧select中所有的key
for (SelectionKey key: oldSelector.keys()) {
Object a = key.attachment();
try {
Object a = key.attachment();
//代码省略 //获取key注册的事件
int interestOps = key.interestOps();
//将key注册的事件取消
key.cancel();
//注册到重新创建的新的selector中
SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
//如果channel是NioChannel
if (a instanceof AbstractNioChannel) {
//重新赋值
((AbstractNioChannel) a).selectionKey = newKey;
}
nChannels ++;
} catch (Exception e) {
//代码省略
}
}
} catch (ConcurrentModificationException e) {
continue;
}
break;
}
selector = newSelector;
//代码省略
}

首先会判断是不是当前NioEventLoop线程执行的, 如果不是, 则将构建方法封装成task由当前NioEventLoop执行

final Selector oldSelector = selector 表示拿到旧的selector

然后通过 newSelector = openSelector() 创建新的selector

通过for循环遍历所有注册在selector中的key

Object a = key.attachment() 是获取channel, 第一章讲过, 在注册时, 将自身作为属性绑定在key上

for循环体中, 通过 int interestOps = key.interestOps() 获取其注册的事件

key.cancel()将注册的事件进行取消

SelectionKey newKey = key.channel().register(newSelector, interestOps, a) 将channel以及注册的事件注册在新的selector中

if (a instanceof AbstractNioChannel) 判断是不是NioChannel

如果是NioChannel, 则通过 ((AbstractNioChannel) a).selectionKey = newKey 将自身的属性selectionKey赋值为新返回的key

selector = newSelector 将自身NioEventLoop属性selector赋值为新创建的newSelector

至此, 就是netty解决epoll bug的步骤, 其实就是创建一个新的selector, 将旧selector中注册的channel和事件重新注册到新的selector中, 然后将自身selector属性替换成新创建的selector

上一节: 优化selector

下一节: 处理IO事件

Netty源码分析第2章(NioEventLoop)---->第6节: 执行select操作的更多相关文章

  1. Netty源码分析第2章(NioEventLoop)---->第8节: 执行任务队列

      Netty源码分析第二章: NioEventLoop   第八节: 执行任务队列 继续回到NioEventLoop的run()方法: protected void run() { for (;;) ...

  2. Netty源码分析第2章(NioEventLoop)---->第1节: NioEventLoopGroup之创建线程执行器

    Netty源码分析第二章: NioEventLoop 概述: 通过上一章的学习, 我们了解了Server启动的大致流程, 有很多组件与模块并没有细讲, 从这个章开始, 我们开始详细剖析netty的各个 ...

  3. Netty源码分析第2章(NioEventLoop)---->第2节: NioEventLoopGroup之NioEventLoop的创建

    Netty源码分析第二章: NioEventLoop   第二节: NioEventLoopGroup之NioEventLoop的创建 回到上一小节的MultithreadEventExecutorG ...

  4. Netty源码分析第2章(NioEventLoop)---->第3节: 初始化线程选择器

    Netty源码分析第二章:NioEventLoop   第三节:初始化线程选择器 回到上一小节的MultithreadEventExecutorGroup类的构造方法: protected Multi ...

  5. Netty源码分析第2章(NioEventLoop)---->第4节: NioEventLoop线程的启动

    Netty源码分析第二章: NioEventLoop   第四节: NioEventLoop线程的启动 之前的小节我们学习了NioEventLoop的创建以及线程分配器的初始化, 那么NioEvent ...

  6. Netty源码分析第2章(NioEventLoop)---->第5节: 优化selector

    Netty源码分析第二章: NioEventLoop   第五节: 优化selector 在剖析selector轮询之前, 我们先讲解一下selector的创建过程 回顾之前的小节, 在创建NioEv ...

  7. Netty源码分析第2章(NioEventLoop)---->第7节: 处理IO事件

    Netty源码分析第二章: NioEventLoop   第七节:处理IO事件 上一小节我们了解了执行select()操作的相关逻辑, 这一小节我们继续学习select()之后, 轮询到io事件的相关 ...

  8. Netty源码分析第4章(pipeline)---->第7节: 前章节内容回顾

    Netty源码分析第四章: pipeline 第七节: 前章节内容回顾 我们在第一章和第三章中, 遗留了很多有关事件传输的相关逻辑, 这里带大家一一回顾 首先看两个问题: 1.在客户端接入的时候, N ...

  9. Netty源码分析第5章(ByteBuf)---->第10节: SocketChannel读取数据过程

    Netty源码分析第五章: ByteBuf 第十节: SocketChannel读取数据过程 我们第三章分析过客户端接入的流程, 这一小节带大家剖析客户端发送数据, Server读取数据的流程: 首先 ...

随机推荐

  1. Oracle-本地连接没问题,远程连接有问题解决方式

    1. 问题 我的环境是oracle11gr2,本地用plsql还是toad连接都没有不论什么问题,而用别的机器远程连接就提示 无监听程序等错误. 2. 原因 oracle安装路径  --- 比如: F ...

  2. BZOJ5329:[SDOI2018]战略游戏(圆方树,虚树)

    Description 省选临近,放飞自我的小Q无心刷题,于是怂恿小C和他一起颓废,玩起了一款战略游戏. 这款战略游戏的地图由n个城市以及m条连接这些城市的双向道路构成,并且从任意一个城市出发总能沿着 ...

  3. 【转】使用URL SCHEME启动天猫客户端并跳转到某个商品页面的方法

    在项目中遇到了这样一个需求:让用户在手机应用中,点击一个天猫的商品链接(知道商品在PC浏览器里的地址),直接启动天猫的客户端并显示这个商品.以前曾经实现过类似的功能,不过那次是淘宝的商品,天猫和淘宝的 ...

  4. HBase学习之路 (九)HBase phoenix的使用

    HBase phoenix的下载 下载地址http://mirror.bit.edu.cn/apache/phoenix/ 选择对应的hbase版本进行下载,测试使用的是hbase-1.2.6版本

  5. Python学习之路 (四)爬虫(三)HTTP和HTTPS

    HTTP和HTTPS HTTP协议(HyperText Transfer Protocol,超文本传输协议):是一种发布和接收 HTML页面的方法. HTTPS(Hypertext Transfer ...

  6. 有关linqtosql和EF的区别

    LINQ to SQL和Entity Framework都是一种包含LINQ功能的对象关系映射技术.他们之间的本质区别在于EF对数据库架构和我们查询的类型实行了更好的解耦.使用EF,我们查询的对象不再 ...

  7. python logging模块按天滚动简单程序

    简单日志按天滚动,加入apsheduler,用crontab模式按小时运行测试: import logging from logging.handlers import TimedRotatingFi ...

  8. B. Vile Grasshoppers

    http://codeforces.com/problemset/problem/937/B The weather is fine today and hence it's high time to ...

  9. traefik-Ingress边缘路由器落地到微服务

    1.理解Ingress 简单的说,ingress就是从kubernetes集群外访问集群的入口,将用户的URL请求转发到不同的service上.Ingress相当于nginx.apache等负载均衡方 ...

  10. 预编译指令#ifdef #endif

    这个是C中的.意思是说如果你定义了某个东西,则执行一段代码,这段代码是包含在ifdef到endif之间的.比如,你debug一个程序,但是到最后你需要将debug的代码删掉,很多则很麻烦.但是如果你那 ...