Netty 核心代码分析
背景
都说是 netty 的 write() 在非 EventLoop 线程调用时线程安全的。但是没看过源码,总是觉得不安心。可能之前自己在写 Reactor 的时候也总是在考虑怎么解决 Reactor 线程 和普通线程 write() 怎么解决竞争问题。所以抱着学习的态度学习了下 Netty 的 EventLoop 对各种事件的处理过程(包括了我一直纠结的 write()。
准备知识
java nio
这不是 java nio 的教程,所以我们只是描述下主要流程(不仅仅是 java 语言,其他的编程语言实现多路复用基本上步骤是一样的)。
1.初始化一个多路复用器
selector = Selector.open()
2.注册感兴趣的 channel 事件。
channel.register(selector, SelectionKey.OP_READ| SelectionKey.OP_WRITE);
3.等待事件发生并处理
while(true){
if(selector.select(TIMEOUT) == 0){
System.out.println("==");
continue;
}
//遍历处理所有的事件
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
//准备好接受新链接
if(key.isAcceptable()){
handleAccept(key);
}
//处理可读事件的请求
if(key.isReadable()){
handleRead(key);
}
//处理可写事件的请求
if(key.isWritable() && key.isValid()){
handleWrite(key);
}
iter.remove();
}
}
EventLoop 的类图
先看下 NioEventLoop 的类图。通过这个类图你只需要记住一点就是 NioEventLoop 是单线程,有序处理任务的线程池就好了。
NioEventLoop 核心代码
SingleThreadEventExecutor 是一个线程池,所以 excute() 会执行。
excute() 执行最终会调用子类 NioEventLoop 的 run() 方法。
NioEventLoop 核心部分 run()。
//ioRate 是指此线程执行 io 操作占时百分比,默认是 50
protected void run() {
for (;;) {
try {
//没有任务,继续 select() 等待满足唤醒条件的操作(wakeup,事件发生),有任务执行 selectNow() 返回准备好事件的key的个数
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//继续 select()
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
// fall through
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
//ioRatio 为 100 的时候就执行完 io 事件之后执行所有的 task
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
//处理有事件发生的 key
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
//计算出执行普通任务占的百分比
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
核心部分为 processSelectedKeys(); 处理有事件的所有的 key 的processSelectedKeys(); -> processSelectedKeysPlain()
循环处理所有有事件发生的 key
Iterator<SelectionKey> i = selectedKeys.iterator();
for (;;) {
final SelectionKey k = i.next();
//
final Object a = k.attachment();
i.remove();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (!i.hasNext()) {
break;
}
}
接着看 processSelectedKey() 处理一个 key 的事件。
下面分别有对应的 accept write read 等事件的处理
try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a 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();
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
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();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
以上就已经基本清楚了, netty 对 io 的操作 nio Selector 编程的几个步骤都是在 EventLoop 这个单线程中完成的。
好了,至于 netty 为什么在其他线程write() 是线程安全的。是怎么实现的,在下节在讨论。