Netty NioEventLoop源码解读

Netty NioEventLoop

Reactor 模型

Netty实现并扩展了Reactor模型,为了更好的了解EventLoop,我们有必要先看一下Reactor模型的定义。

Reactor.png

在wiki对reactor pattern的定义中,指出了一下集中角色:

  • Resource:资源指的是提供系统输入或者消费系统输出的资源。在Netty中它指的是SocketChannel,它们应支持select。
  • Demultiplexer:事件分离器负责对资源进行轮寻等待,当资源ready的时候,分离器负责将数据发送给Dispatcher。
  • Dispatcher:处理Handler的注册和反注册。当资源到达时负责把资源分发到相应的Handler中。
  • Handler:负责处理数据。

在Netty中EventLoop兼负了Demultiplexer以及Dispatcher两个角色。下边我们通过来看NioEventLoop的源码学习学习并了解Netty中的EventLoop。

EventLoop源码

NioEventLoop的核心方法是run()方法,一旦Netty程序启动之后,这个就一直循环跑下去,不间断的查询IO和处理task。

protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }
        }
        ///
}

关于selectionStrategy

首先我们来看第一个逻辑Select Strategy。这段逻辑主要控制这次循环是执行:跳过;select操作;还是fall through。判断依据是这样的:

public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
    return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}

如果当前EventLoop中有未处理的task,则执行selectorNowSupplier。selectorNowSupplier调用了selectNow。selectNow调用的是Selector的selectNow这个非阻塞方法。执行完selectNow则跳出switch运行下边的processSelectedKeys逻辑。

为了高效的利用CPU,EventLoop中只要有未消费的task则优先消费task。

Nio中Selector.select()是阻塞的,直到某个selection key可用select方法才会返回。Selector.selectNow()则检查自从上次select到现在有没有可用的selection key,然后立即返回。

private final IntSupplier selectNowSupplier = new IntSupplier() {
    @Override
    public int get() throws Exception {
        return selectNow();
    }
};

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

select操作

select操作主要是检查当前的selection key,看哪些已available。

上边我们说到了Selector.select操作是阻塞的,那么如果我不想等了,可以中断它吗?可以,Selector.wakeup可以唤醒正在阻塞的select()操作。但是如果当前没有select操作,执行了wakeUp操作,那么下次执行的select()或者selectNow()操作将被立即唤醒。

但是Selector.wakeup是开销比较大的操作,不能每次都直接调用wakeup,于是NioEventLoop中声明了wakenUp(AtomicBoolean)字段,用于控制selector.wakeup()的调用。调用wakeup之前先wakenUp.compareAndSet(false, true),如果set成功才执行Selector.wakeup()操作。

当用户提交新的任务时executor.execute(...),会触发wakeup操作。

select(wakenUp.getAndSet(false));

if (wakenUp.get()) {
    selector.wakeup();
}

这段代码有一段非常长的注释,解释了为什么这段逻辑这样实现。并且给出了什么情况下会产生竞态条件:

wakenUp.set(false)
selector.select(...)

wakenUp.set(false)执行后,用户出发了wakeup操作,然后执行select操作,这时select将立即返回。直到下次循环把wakenUp重置为false,期间所有的wakenUp.compareAndSet(false, true)都是执行失败的,因为现在wakenUp的值是true。所以接下来的select()都不能被wakeup。

select 内部逻辑

接下来我们看select是如何实现的:

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 (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) { // 1
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }

                if (hasTasks() && wakenUp.compareAndSet(false, true)) { // 2
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;

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

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    // timeoutMillis elapsed without anything selected.
                    selectCnt = 1;
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    logger.warn(
                            "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                            selectCnt, selector);

                    // 重建Selector,旧的Selector中的Selection Key要拷贝到新的Selector中
                    rebuildSelector();
                    selector = this.selector;

                    // Select again to populate selectedKeys.
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }
        ///    
    }

selectCnt标记select执行的次数,用于检测NIO的epoll bug。在这个方法尾部有一个判断:

 if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {}

判断select记次是否超过了伐值,如果是的话有可能触发了Nio epoll bug,执行重建selector的逻辑:新建一个Selector,把原来老的selection key都复制过去。重建完成之后再执行一次selectNow。

因为select操作是阻塞的,如果长时间没有IO可用,就会造成NioEventLoop中的task积压。因此每次执行select操作都设定一个超时:
1.查询定时任务重最近要被执行的task还有多长时间执行.
2.这个时间加上0.5ms就是最大超时时间。

long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

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

整体来看一下这个for循环:

  • 第1个if:如果timeoutMillis小于0,则立即执行一次异步的selectNow,跳出循环消费task。
  • 第2个if:如果当前taskQueue中有task,并且没有被wakeup,则执行一次异步的selectNow,跳出循环消费task。
  • 接下来执行select,并记次。
  • 第3个if:如果有available keys 或者 被用户唤醒 或者 任务队列定时队列有任务则中断。
  • 最后就是重建selector的过程。

processSelectedKeys

cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
    try {
        processSelectedKeys();
    } finally {
        runAllTasks();
    }
} else {
    final long ioStartTime = System.nanoTime();
    try {
        processSelectedKeys();
    } finally {
        final long ioTime = System.nanoTime() - ioStartTime;
        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }
}

NioEventLoop.run方法的后半段逻辑主要是processSelectedKeys(处理IO)和runTasks(消费任务)。这里有一个参数用于控制处理这两种任务的时间配比:ioRatio。

先来看一下processSelectedKeys,它的逻辑由processSelectedKeysOptimized和processSelectedKeysPlain实现,调用那个函数取决于你是否开启了DISABLE_KEYSET_OPTIMIZATION。如果开启了Selection 优化选项,则在创建Selector的时候以反射的方式把SelectedSelectionKeySet selectedKeys设置到selector中。具体实现在openSelector中,代码就不贴出来了。SelectedSelectionKeySet内部是基于Array实现的,而Selector内部selectedKeys是Set类型的,遍历效率Array效率更好一下。

我们来分析processSelectedKeysPlain方法:

private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
    if (selectedKeys.isEmpty()) {
        return;
    }

    Iterator<SelectionKey> i = selectedKeys.iterator();
    for (;;) {
        final SelectionKey k = i.next();
        final Object a = k.attachment();
        i.remove();

        // 处理channel中的数据
        if (a instanceof AbstractNioChannel) {
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
            @SuppressWarnings("unchecked")
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }

        if (!i.hasNext()) {
            break;
        }

        if (needsToSelectAgain) {
            selectAgain();
            selectedKeys = selector.selectedKeys();

            // Create the iterator again to avoid ConcurrentModificationException
            if (selectedKeys.isEmpty()) {
                break;
            } else {
                i = selectedKeys.iterator();
            }
        }
    }
}

SelectionKey上边可以挂载Attachment,一般情况下新的链接对象Channel会挂到attachment上。我们在遍历selectedKeys时,首先取出selection key上的attachment,key的类型可能是AbstractNioChannel和NioTask。根据不同的类型调用不同的处理函数。我们着重看处理channel的逻辑:

1.如果selection key是:SelectionKey.OP_CONNECT,那表明这是一个链接操作。对于链接操作,我们需要把这个selection key从intrestOps中清除掉,否则下次select操作会直接返回。接下来调用finishConnect方法。

2.如果selection key是:SelectionKey.OP_WRITE。则执行flush操作,把数据刷到客户端。

3.如果是read操作则调用unsafe.read()。这个操作就不展开了,等到接下来的文章,专门分析read操作。

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        try {
            int readyOps = k.readyOps();
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

            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();
            }

            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

整体来看NioEventLoop的实现也不复杂,主要就干了两件事情:select IO以及消费task。因为select操作是阻塞的(尽管设置了超时时间),每次执行select时都会检查是否有新的task,有则优先执行task。这么做也是做大限度的提高EventLoop的吞吐量,减少阻塞时间。

除了这两件事儿呢,NioEventLoop还解决了JDK中注明的EPoll bug。到此NioEventLoop源码分析完结。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容