Netty線程源碼分析(一)

一、NioEventLoopGroup

繼承關系圖1-1:


1
1

Netty允許處理IO和接收連接使用同一個EventLoopGroup


1
1

1.1 NioEventLoopGroup和NioEventLoop是什么關系?

NioEventLoopGroup實際是NioEventLoop的線程組,它包含了一個或多個EventLoop,而EventLoop就是一個Channel執(zhí)行實際工作的線程,當注冊一個Channel后,Netty將這個Channel綁定到一個EventLoop上,在其生命周期內(nèi)始終被綁定在這個EventLoop上不會被改變。

從圖1-2可以看出很多Channle會共享一個EventLoop。這意味著在一個Channel在被EventLoop使用時會禁止其它Channel綁定到相同的EventLoop。我們可以理解為EventLoop是一個事件循環(huán)線程,而EventLoopGroup是一個事件循環(huán)集合。

圖1-2:

1
1

1.2 NioEventLoopGroup初始化

首先看NioEventLoopGroup構造方法:

public NioEventLoopGroup() {
    this(0);
}

public NioEventLoopGroup(int nThreads) {
    this(nThreads, null);
}

public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
    this(nThreads, threadFactory, SelectorProvider.provider());
}

public NioEventLoopGroup(
        int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider) {
    super(nThreads, threadFactory, selectorProvider);
}

以上代碼可以發(fā)現(xiàn)NioEventLoopGroup雖然有4個構造方法,但最終調(diào)用的是MultithreadEventLoopGroup的構造方法,代碼如下:

protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {  
    super(nThreads == 0? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);  
}

private static final int DEFAULT_EVENT_LOOP_THREADS;

    static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }

在NioEventLoopGroup初始化之前,會先執(zhí)行父類MultithreadEventLoopGroup的靜態(tài)模塊,NioEventLoop的默認大小是2倍的CPU核數(shù),但這并不是一個恒定的最佳數(shù)量,為了避免線程上下文切換,只要能滿足要求,這個值其實越少越好。

MultithreadEventExecutorGroup的構造方法:

//EventExecutor數(shù)組,保存eventLoop
private final EventExecutor[] children;
//從children中選取一個eventLoop的策略
private final EventExecutorChooser chooser;

protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
    if (nThreads <= 0) {
        throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
    }

    if (threadFactory == null) {
        //是一個通用的ThreadFactory實現(xiàn),方便配置線程池
        threadFactory = newDefaultThreadFactory();
    }
    
    //根據(jù)線程數(shù)創(chuàng)建SingleThreadEventExecutor數(shù)組,從命名上可以看出SingleThreadEventExecutor是一個只有一個線程的線程池
    children = new SingleThreadEventExecutor[nThreads];
    //根據(jù)數(shù)組的大小,采用不同策略初始化chooser。
    if (isPowerOfTwo(children.length)) {
        chooser = new PowerOfTwoEventExecutorChooser();
    } else {
        chooser = new GenericEventExecutorChooser();
    }

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            //
            children[i] = newChild(threadFactory, args);
            success = true;
        } catch (Exception e) {
            // TODO: Think about if this is a good exception type
            throw new IllegalStateException("failed to create a child event loop", e);
        } finally {
            //如果沒有創(chuàng)建成功,循環(huán)關閉所有SingleThreadEventExecutor
            if (!success) {
                for (int j = 0; j < i; j ++) {
                    children[j].shutdownGracefully();
                }

                //等待關閉成功
                for (int j = 0; j < i; j ++) {
                    EventExecutor e = children[j];
                    try {
                        while (!e.isTerminated()) {
                            e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                        }
                    } catch (InterruptedException interrupted) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
    }

    final FutureListener<Object> terminationListener = new FutureListener<Object>() {
        @Override
        public void operationComplete(Future<Object> future) throws Exception {
            if (terminatedChildren.incrementAndGet() == children.length) {
                terminationFuture.setSuccess(null);
            }
        }
    };

    for (EventExecutor e: children) {
        e.terminationFuture().addListener(terminationListener);
    }
}

回到NioEventLoopGroup的newChild方法重載

@Override
protected EventExecutor newChild(
        ThreadFactory threadFactory, Object... args) throws Exception {
    return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0]);
}

MultithreadEventExecutorGroup構造方法中執(zhí)行的是NioEventLoopGroup中的newChild方法,所以children元素的實際類型是NioEventLoop。

解釋下EventExecutorChooser的選擇

    //判斷一個數(shù)是否是2的冪次方
    private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }
    
    private final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        @Override
        public EventExecutor next() {
            return children[childIndex.getAndIncrement() & children.length - 1];
        }
    }

    private final class GenericEventExecutorChooser implements EventExecutorChooser {
        @Override
        public EventExecutor next() {
            return children[Math.abs(childIndex.getAndIncrement() % children.length)];
        }
    }

EventExecutorChooser是根據(jù)線程數(shù)組大小是否是2的冪次方來選擇初始化chooser。如果大小為2的冪次方,則采用PowerOfTwoEventExecutorChooser,否則使用GenericEventExecutorChooser。也就是如果線程數(shù)是2的倍數(shù)時,Netty選擇線程時會使用PowerOfTwoEventExecutorChooser,因為&比%更快(Netty為了性能也是拼了).

二、NioEventLoop

NioEventLoop中維護了一個線程,里面有一個任務隊列和一個延遲任務隊列,每個EventLoop有一個Selector,這里強制借用一張圖而且不留地址!

image

繼承關系:


1
1

NioEventLoop初始化

NioEventLoop(NioEventLoopGroup parent, ThreadFactory threadFactory, SelectorProvider selectorProvider) {
    super(parent, threadFactory, false);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    provider = selectorProvider;
    selector = openSelector();
}

1、調(diào)用父類方法構造一個taskQueue,它是一個LinkedBlockingQueue

2、openSelector(): Netty是基于Nio實現(xiàn)的,所以也離不開selector。

3、DISABLE_KEYSET_OPTIMIZATION: 判斷是否需要對sun.nio.ch.SelectorImpl中的selectedKeys進行優(yōu)化, 不做配置的話默認需要優(yōu)化,通過反射將selectedKeySet與sun.nio.ch.SelectorImpl中的兩個field綁定

4、主要優(yōu)化在哪: SelectorImpl原來的selectedKeys和publicSelectedKeys數(shù)據(jù)結構是HashSet,大家知道HashSet的數(shù)據(jù)結構是數(shù)組+鏈表,新的數(shù)據(jù)結構是由2個數(shù)組A、B組成,初始大小是1024,避免了HashSet擴容帶來的性能問題。除了擴容外,遍歷效率也是一個原因,對于需要遍歷selectedKeys的全部元素, 數(shù)組效率無疑是最高的。

private Selector openSelector() {
    final Selector selector;
    try {
        selector = provider.openSelector();
    } catch (IOException e) {
        throw new ChannelException("failed to open a new selector", e);
    }

    if (DISABLE_KEYSET_OPTIMIZATION) {
        return selector;
    }

    try {
        SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

        Class<?> selectorImplClass =
                Class.forName("sun.nio.ch.SelectorImpl", false, PlatformDependent.getSystemClassLoader());

        // Ensure the current selector implementation is what we can instrument.
        if (!selectorImplClass.isAssignableFrom(selector.getClass())) {
            return selector;
        }

        Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
        Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

        selectedKeysField.setAccessible(true);
        publicSelectedKeysField.setAccessible(true);

        selectedKeysField.set(selector, selectedKeySet);
        publicSelectedKeysField.set(selector, selectedKeySet);

        selectedKeys = selectedKeySet;
        logger.trace("Instrumented an optimized java.util.Set into: {}", selector);
    } catch (Throwable t) {
        selectedKeys = null;
        logger.trace("Failed to instrument an optimized java.util.Set into: {}", selector, t);
    }

    return selector;
}

NioEventLoop的啟動

在上一遍講過NioEventLoop中維護了一個線程,線程啟動時會調(diào)用NioEventLoop的run方法,loop會不斷循環(huán)一個過程:select -> processSelectedKeys(IO任務) -> runAllTasks(非IO任務)

  • I/O任務: 即selectionKey中ready的事件,如accept、connect、read、write等

  • 非IO任務: 添加到taskQueue中的任務,如bind、channelActive等

@Override
protected void run() {
    for (;;) {
        boolean oldWakenUp = wakenUp.getAndSet(false);
        try {
            // 判斷是否有非IO任務,如果有立刻返回
            if (hasTasks()) {
                selectNow();
            } else {
                select(oldWakenUp);

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

            cancelledKeys = 0;
            needsToSelectAgain = false;
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                // IO任務
                processSelectedKeys();
                // 非IO任務
                runAllTasks();
            } else {
                // 用以控制IO任務與非IO任務的運行時間比
                final long ioStartTime = System.nanoTime();
                // IO任務
                processSelectedKeys();

                final long ioTime = System.nanoTime() - ioStartTime;
                // 非IO任務
                runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
            }

            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    break;
                }
            }
        } catch (Throwable t) {
            // Prevent possible consecutive immediate failures that lead to
            // excessive CPU consumption.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Ignore.
            }
        }
    }
}

1、wakenUp: 用來決定是否調(diào)用selector.wakeup(),只有當wakenUp未true時才會調(diào)用,目的是為了減少wake-up的負載,因為Selector.wakeup()是一個昂貴的操作。

2、hasTask(): 判斷是否有非IO任務,如果有的話,選擇調(diào)用非阻塞的selectNow()讓select立即返回, 否則以阻塞的方式調(diào)用select.timeoutMillis是阻塞時間

3、ioRatio: 控制兩種任務的執(zhí)行時間,你可以通過它來限制非IO任務的執(zhí)行時間, 默認值是50, 表示允許非IO任務獲得和IO任務相同的執(zhí)行時間,這個值根據(jù)自己的具體場景來設置.

4、processSelectedKeys(): 處理IO事件

5、runAllTasks(): 處理非IO任務

6、isShuttingDown(): 檢查state是否被標記為ST_SHUTTING_DOWN

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) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }

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

                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    // - Selected something,
                    // - waken up by user, or
                    // - the task queue has a pending task.
                    // - a scheduled task is ready for processing
                    break;
                }
                if (Thread.interrupted()) {
                    // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                    // As this is most likely a bug in the handler of the user or it's client library we will
                    // also log it.
                    //
                    // See https://github.com/netty/netty/issues/2426
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because " +
                                "Thread.currentThread().interrupt() was called. Use " +
                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }
                    selectCnt = 1;
                    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) {
                    // The selector returned prematurely many times in a row.
                    // Rebuild the selector to work around the problem.
                    logger.warn(
                            "Selector.select() returned prematurely {} times in a row; rebuilding selector.",
                            selectCnt);

                    rebuildSelector();
                    selector = this.selector;

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

                currentTimeNanos = time;
            }

            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
                }
            }
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
            }
        }
    }
protected long delayNanos(long currentTimeNanos) {
    ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
    if (scheduledTask == null) {
        return SCHEDULE_PURGE_INTERVAL;
    }

    return scheduledTask.delayNanos(currentTimeNanos);
}

public long delayNanos(long currentTimeNanos) {
    return Math.max(0, deadlineNanos() - (currentTimeNanos - START_TIME));
}

public long deadlineNanos() {
    return deadlineNanos;
}

1、delayNanos(currentTimeNanos): 在父類SingleThreadEventExecutor中有一個延遲執(zhí)行任務的隊列,delayNanos就是去這個延遲隊列里看是否有非IO任務未執(zhí)行

  • 如果沒有則返回1秒鐘。
  • 如果延遲隊列里有任務并且最終的計算出來的時間(selectDeadLineNanos - currentTimeNanos)小于500000L納秒,就調(diào)用selectNow()直接返回,反之執(zhí)行阻塞的select

2、select如果遇到以下幾種情況會立即返回

if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    // - Selected something,
                    // - waken up by user, or
                    // - the task queue has a pending task.
                    // - a scheduled task is ready for processing
                    break;
                }
  1. Selected something 如果select到了就緒連接(selectedKeys > 0)
  2. waken up by user 被用戶喚醒了
  3. the task queue has a pending task.任務隊列來了一個新任務
  4. a scheduled task is ready for processing 延遲隊列里面有個預約任務需要到期執(zhí)行

3、selectCnt: 記錄select空轉的次數(shù)(selectCnt),該方法解決了Nio中臭名昭著selector的select方法導致cpu100%的BUG,當空轉的次數(shù)超過了512(定義一個閥值,這個閥值默認是512,可以在應用層通過設置系統(tǒng)屬性io.netty.selectorAutoRebuildThreshold傳入),Netty會重新構建新的Selector,將老Selector上注冊的Channel轉移到新建的Selector上,關閉老Selector,用新的Selector代替老Selector。詳細看下面rebuildSelector()方法

4、rebuildSelector(): 就是上面說過得。

public void rebuildSelector() {
    if (!inEventLoop()) {
        execute(new Runnable() {
            @Override
            public void run() {
                rebuildSelector();
            }
        });
        return;
    }

    final Selector oldSelector = selector;
    final Selector newSelector;

    if (oldSelector == null) {
        return;
    }

    try {
        newSelector = openSelector();
    } catch (Exception e) {
        logger.warn("Failed to create a new Selector.", e);
        return;
    }

    // Register all channels to the new Selector.
    int nChannels = 0;
    for (;;) {
        try {
            for (SelectionKey key: oldSelector.keys()) {
                Object a = key.attachment();
                try {
                    if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
                        continue;
                    }

                    int interestOps = key.interestOps();
                    key.cancel();
                    SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
                    if (a instanceof AbstractNioChannel) {
                        // Update SelectionKey
                        ((AbstractNioChannel) a).selectionKey = newKey;
                    }
                    nChannels ++;
                } catch (Exception e) {
                    logger.warn("Failed to re-register a Channel to the new Selector.", e);
                    if (a instanceof AbstractNioChannel) {
                        AbstractNioChannel ch = (AbstractNioChannel) a;
                        ch.unsafe().close(ch.unsafe().voidPromise());
                    } else {
                        @SuppressWarnings("unchecked")
                        NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                        invokeChannelUnregistered(task, key, e);
                    }
                }
            }
        } catch (ConcurrentModificationException e) {
            // Probably due to concurrent modification of the key set.
            continue;
        }

        break;
    }

    selector = newSelector;

    try {
        // time to close the old selector as everything else is registered to the new one
        oldSelector.close();
    } catch (Throwable t) {
        if (logger.isWarnEnabled()) {
            logger.warn("Failed to close the old Selector.", t);
        }
    }

    logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內(nèi)容