3.NioEventLoop的启动和执行

NioEventLoop启动和执行

NioEventLoop启动

在服务端启动的代码中,我们看到netty在注册和绑定时,判断了当前线程是否是NioEventLoop线程。如果不是,
则将这些操作包装成一个任务丢到EventExecutor中来完成。

// 调用SingleThreadEventExecutor对象的execute方法
eventLoop.execute(() -> register0(promise));

// SingleThreadEventExecutor对象的execute方法
@Override
public void execute(Runnable task) {
    boolean inEventLoop = inEventLoop();
    addTask(task);
    if (!inEventLoop) {
        startThread();
    }

    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

在execute方法中,再次判断是否是NioEventLoop线程,若不是则执行startThread方法。startThread方法通过CAS
将线程的state修改为已启动,成功后进入doStartThread方法。这个方法包装了一个任务,交由在创建NioEventLoop
时设置的Executor执行。默认情况下,它是ThreadPerTaskExecutor,也因此,它会启动一个新的线程执行任务。包装
任务的主要逻辑有3个:

  1. 将当前线程与nioEventLoop绑定;
  2. 更新上次执行的时长为当前时间-上个任务启动时间;
  3. 执行NioEventLoop的run方法;
private void doStartThread() {
    executor.execute(() -> {
        thread = Thread.currentThread();
        updateLastExecutionTime();
        SingleThreadEventExecutor.this.run();
    });
}

至此,NioEvnetLoop就启动了。

NioEventLoop执行

当NioEventLoop启动后,就开始执行SingleThreadEventExecutor的run方法。此方法是一个死循环,也可以分为3个步骤

  1. 轮询channel中就绪的IO事件
  2. 处理轮询出的IO事件
  3. 处理所有任务,也包括定时任务

轮询事件

整个轮询IO事件的流程如下

switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
    case SelectStrategy.CONTINUE:
        continue;
    case SelectStrategy.BUSY_WAIT:
    case SelectStrategy.SELECT:
        select(wakenUp.getAndSet(false));
        if (wakenUp.get()) {
            selector.wakeup();
        }
    default:
}

在循环的开始阶段,调用选择策略器选择select策略,默认策略下,先判断是否有任务,若没有任务,调用selectNow(),否则进入SelectStrategy.SELECT,也即调用select(wakeUp.getAndSet(false))。

selectNow()方法

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

nioEventLoop的selectNow方法会调用持有的Selector对象的selectNow方法。此方法轮询后,即使没有事件也会立即返回,而selector.select方法则会阻塞。
finally操作保证当wakenUp字段为true时,调用一次selector.wakeup方法,此方法会使阻塞的select方法唤醒,若当前没有select阻塞,则下一次select会立即返回。

select(boolean oldWakenUp)方法

首先看到入参为wakeup.getAndSet(false)。wakeup的作用稍后分析,这里简单提一下它的作用是控制将阻塞的selector唤醒。
详细代码如下

private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
int selectCnt = 0;
// 步骤1
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
long normalizedDeadlineNanos = selectDeadLineNanos - initialNanoTime();
if (nextWakeupTime != normalizedDeadlineNanos) {
    nextWakeupTime = normalizedDeadlineNanos;
}
for (;;) {
    // 步骤2
    long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
    if (timeoutMillis <= 0) {
        if (selectCnt == 0) {
            selector.selectNow();
            selectCnt = 1;
        }
        break;
    }
    // 步骤3
    if (hasTasks() && wakenUp.compareAndSet(false, true)) {
        selector.selectNow();
        selectCnt = 1;
        break;
    }
    // 步骤4
    int selectedKeys = selector.select(timeoutMillis);
    selectCnt++;
    if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
        break;
    }

    // 步骤5
    if (Thread.interrupted()) {
        selectCnt = 1;
        break;
    }
    // 步骤6
    long time = System.nanoTime();
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
        selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
        selector = selectRebuildSelector(selectCnt);
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time;
}

代码较长,可以分为6个步骤

  1. 步骤1计算了多种精确到纳秒级别的时间,⑴当前时间;⑵select阻塞截止时间,这里又会根据是否有定时任务来计算,若有到时间的定时任务,则取最近一个定时
    任务的截止时间,若没有定时任务或定时任务还没到时间,则取1秒后;⑶规整化截止时间与下次唤醒时间

从步骤2开始,又进入一个死循环内:

  1. 四舍五入计算阻塞超时时间。若超时时间小于0且空轮询次数为0,执行一次selectNow后返回。
  2. 轮询前先判断有没有任务,若有任务,且wakeup由false设置为true了,则执行selectNow。否则会因为无法唤醒selector耽误这个任务的执行。执行完后,结束本次循环。
  3. 阻塞式select。阻塞结束后,发生下列条件之一时,结束本次循环:⑴轮询到了IO事件;⑵进入select(boolean wakeup)之前,参数oldWakeup为true,也即之前有过wakeup的动作;⑶当前需要唤醒,可能是用户主动调用wakeup方法唤醒的;⑷队列里有任务了,可能是外部线程添加的;⑸有定时任务到期了
  4. 若线程被打断,设置空轮询次数为1,结束此次循环
  5. 根据当前时间与进入方法时计算的时间判断阻塞式select是否超时,若time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos成立,则有time-currentTimeNanos>=timeoutMillis,说明这次select执行的时间不够,可能触发了空轮询,将空轮询次数为1,计算累计空轮询次数是否大于阈值(阈值SELECTOR_AUTO_REBUILD_THRESHOLD默认为512),当大于阈值时,重建selector,以规避JDK空轮询bug。反之,则进行了一次有效的select,将累计空轮询次数置为1,结束本次循环。
规避空轮询bug

其实netty规避空轮询bug的方式也很巧妙,就是通过新建selector,并将旧selector上的key和attchment复制过去

private Selector selectRebuildSelector(int selectCnt) throws IOException {
    rebuildSelector();
    Selector selector = this.select
    // Select again to populate selectedKeys.
    selector.selectNow();
    return selector;
}

private void rebuildSelector0() {
    final Selector oldSelector = selector;
    final SelectorTuple newSelectorTuple
    newSelectorTuple = openSelector();
    // 将老selector的key和attchment传递给新selector
    for (SelectionKey key : oldSelector.keys()) {
        Object a = key.attachment();
        int interestOps = key.interestOps();
        key.cancel();
        SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
        if (a instanceof AbstractNioChannel) {
            // Update SelectionKey
            ((AbstractNioChannel) a).selectionKey = newKey;
        }
    selector = newSelectorTuple.selector;
    unwrappedSelector = newSelectorTuple.unwrappedSelector;
    // 关闭老selector
    oldSelector.close();
    }
}

代码足够详细,就不多加解释了

select(wakeup.getAndSet(false))执行完后,还有这样几行代码①

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

之前提到wakeup的作用是控制将阻塞的selector唤醒。这里先详细说下。
回顾上文,SingleThreadEventExecutor对象的execute方法有一个添加任务后调用wakeup的动作,nioEventLoop重写了wakeup方法如下

protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
        selector.wakeup();
    }
}

这里进行了2个判断,!inEventLoop表明这是外部线程,selector.wakeup()使阻塞的select操作立即唤醒,以便及时处理此时添加的这个任务。
这个方法让外部线程在加入任务时,能及时唤醒selector处理任务
根据netty的解释,wakeup.compareAndSet(false, true)总是在selector.wakeup之前调用,以便在同时多个任务时减少selector.wakeup的性能消耗。

代码①的注释进一步提到存在两种竞态条件使wakeup太早被设置为true。

  1. 如果Selector在wakeup.set(false)和selector.select(timeout)之间被唤醒。这里发生在步骤4之前。
  2. 如果Selector在selector.select()和if(wakeup.get())之间被唤醒。这里发生在步骤4之后。
    在第一种情况,接下来的一次selector.select(timeout)(注:称为select1)将立即唤醒。之后由于wakeup为true,wakeup.compareAndSet(false, true)将失败,从而导致无法调用selector.wakeup,
    假如这期间(从步骤4到下一次selector.select(timeout)(注:称为select2))加入一个任务,那么得等到下一次select超时,任务才能得到处理。
    所以查询完任务后,如果发现wakeup为true,再调用一次selector.wakup()。
    不过细心的读者会留意到,在步骤3的几个条件里,netty会调用hasTask查看任务队列是否有任务,且在进入select方法前,会把wakeup设置为false,所以wakenUp.compareAndSet(false, true)会成功,因此会调用selectNow,而不必等到select2超时才处理任务。
    第二种情况下,select2会立即返回,没有问题。
    那这段代码有何意义?
    实际上笔者个人认为这段代码属于遗留代码,理由是笔者在52im找到了netty3的代码,在netty3中的AbstractNioSelector类中,wakeup设置为false后,直接调用了selector.select(timeout)。在当时看来,这不失为一种解决方案。

到了这里,NioEventLoop完成了启动,并查询出了selectionKey,下一步就是处理selectionKey。
so····未完待续

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

推荐阅读更多精彩内容