自頂向下深入分析Netty(四)--EventLoop-1

netty線程模型

我們再次回顧這幅圖,通過先前的講解,現在是不是親切很多了。圖中綠色的acceptor應該是你最熟悉的部分,之前我們在ServerBootstrap中進行了詳細分析。我們知道了mainReactor是一個線程池,處理Accept事件負責接受客戶端的連接;subReactor也是一個線程池,處理Read(讀取客戶端通道上的數據)、Write(將數據寫入到客戶端通道上)等事件。在這一節中,我們將深入分析這兩個線程池的實現,不斷完善其中的細節。我們首先從類圖開始。

4.1 類圖

EventLoop類圖
EventLoop類圖

看到這幅類圖,如果你的第一印象是氣勢恢宏,那么恭喜你,你已經成功了一半。但不難預料的是,大多數人和我的感受是一樣的:這么多類,一定很累。好在這只是第一印象,我們仔細觀察,便會發現其中明顯的脈絡,兩條線索(這里使用自下而上):NioEventLoop以及NioEventLoopGroup即線程和線程池。忽略其中大量的接口,剩余這樣的兩條線:

NioEventLoop --> SingleThreadEventLoop --> SingleThreadEventExecutor -->
AbstractScheduledEventExecutor --> AbstractScheduledEventExecutor --> 
AbstractEventExecutor --> AbstractExecutorService

NioEventLoopGroup --> MultithreadEventLoopGroup --> 
MultithreadEventExecutorGroup --> AbstractEventExecutorGroup

下面我們正式開始分析,依舊使用自頂向下的方法,從類圖頂部向下、從線程池到線程分析。

4.2 EventExecutorGroup

EventExecutorGroup在類圖中處于承上啟下的位置,其上是Java原生的接口和類,其下是Netty新建的接口和類,由于它處于如此重要的位置,我們詳細分析其中的方法。

4.2.1 Executor

首先看其繼承自Executor的方法:

    // Executes the given command at some time in the future
    void execute(Runnable command);

只有一個簡單的execute()方法,但這個方法奠定了java并發的基礎,提供了異步執行任務的。

4.2.2 ExecutorService

ExecutorService的關鍵方法如下(其中的invoke***方法并非關鍵,不再列出):

    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);

這些方法我們能從命名中便能知道方法的作用。我們主要看submit()方法,該方法是execute()方法的擴展,相較于execute不關心執行結果,submit返回一個異步執行結果Future。這無疑是很大的進步,但這里的Future不提供回調操作,顯得很雞肋,所以Netty將Java原生的java.util.concurrent.Future擴展為io.netty.util.concurrent.Future,我們將在之后進行介紹。

4.2.3 ScheduledExecutorService

從名字可以看出,該接口提供了一系列調度方法:

    ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
    <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
    ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
                                                long period,TimeUnit unit);
    ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,
                                                long delay,TimeUnit unit);

schedule()方法調度任務使任務在延遲一段時間后執行。scheduleAtFixedRate延遲一段時間后以固定頻率執行任務,scheduleWithFixedDelay延遲一段時間后以固定延時執行任務。是不是有點頭暈?那就對了,這里有一個例子專門治頭暈。專家建議程序員應該每小時工作50分鐘,休息10分鐘,類似這樣:

    13:00 - 13:10 休息
    13:10 - 14:00 寫代碼
    14:00 - 14:10 休息
    14:10 - 15:00 寫代碼

實現這樣的調度我們可以使用(假設現在時間為13:00):

    executor.scheduleAtFixedRate(new RestRunnable(), 0 , 60, TimeUnit.MINUTES);
    executor.scheduleWithFixedDelay(new RestRunnable(), 0 , 50, TimeUnit.MINUTES);

4.2.4 EventExecutorGroup

    boolean isShuttingDown();
    Future<?> shutdownGracefully();
    Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit);
    Future<?> terminationFuture();
    EventExecutor next();

EventExecutorGroup擴展的方法有5個,前四個可以從命名中推斷出功能。shutdownGracefully()我們已經在Bootstrap一節中使用過,優雅關閉線程池;terminationFuture()返回線程池終止時的異步結果。重點關注next()方法,該方法的功能是從線程池中選擇一個線程。EventExecutorGroup還覆蓋了一些方法,我們不再列出,如果你感興趣可以去源碼里面查看,需要注意的是,覆蓋的方法大部分是將Java原生的java.util.concurrent.Future返回值覆蓋為io.netty.util.concurrent.Future。

4.3 線程池

4.3.1 AbstractEventExecutorGroup

AbstractEventExecutorGroup實現了EventExecutorGroup接口的大部分方法,實現都長的和下面的差不多:

    @Override
    public void execute(Runnable command) {
        next().execute(command);
    }

從這段代碼可以看出這個線程池和程序員有一個相同點:懶。當線程池執行一個任務或命令時,步驟是這樣的:(1).找一個線程。(2).交給線程執行。

4.3.2 MultithreadEventExecutorGroup

MultithreadEventExecutorGroup實現了線程的創建和線程的選擇,其中的字段為:

    // 線程池,數組形式可知為固定線程池
    private final EventExecutor[] children;
    // 線程索引,用于線程選擇
    private final AtomicInteger childIndex = new AtomicInteger();
    // 終止的線程個數
    private final AtomicInteger terminatedChildren = new AtomicInteger();
    // 線程池終止時的異步結果
    private final Promise<?> terminationFuture = 
                          new DefaultPromise(GlobalEventExecutor.INSTANCE);
    // 線程選擇器
    private final EventExecutorChooser chooser;

MultithreadEventExecutorGroup的構造方法很長,我們將選出其中的關鍵部分分析,故不列出整體代碼。如果你是處女座,這里有一個鏈接MultithreadEventExecutorGroup
我們先看構造方法簽名:

    protected MultithreadEventExecutorGroup(int nThreads, 
                                        ThreadFactory threadFactory, Object... args)

其中的nThreads表示線程池的固定線程數。
MultithreadEventExecutorGroup初始化的步驟是:
(1).設置線程工廠
(2).設置線程選擇器
(3).實例化線程
(4).設置線程終止異步結果
首先我們看設備線程工廠的代碼:

    if (threadFactory == null) {
        threadFactory = newDefaultThreadFactory();
    }
    
    protected ThreadFactory newDefaultThreadFactory(),() {
        return new DefaultThreadFactory(getClass());
    }

如果構造參數threadFactory為空則使用默認線程池,創建默認線程池使用newDefaultThreadFactory(),這是一個protected方法,可以在子類中覆蓋實現。
接著我們看設置線程選擇器的代碼:

    if (isPowerOfTwo(children.length)) {
        chooser = new PowerOfTwoEventExecutorChooser();
    } else {
        chooser = new GenericEventExecutorChooser();
    }

如果線程數是2的冪次方使用2的冪次方選擇器,否則使用通用選擇器。下次如果有面試官問你怎么判斷一個整數是2的冪次方,請甩給他這一行代碼:

    private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }

Netty實現了兩個線程選擇器,雖然代碼不一致,功能都是一樣的:每次選擇索引為上一次所選線程索引+1的線程。如果你沒看明白代碼的含義,沒關系,再看一遍。

    private interface EventExecutorChooser {
        EventExecutor next();
    }
    
    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)];
        }
    }

最佳實踐:線程池數量使用2的冪次方,這樣線程池選擇線程時使用位操作,能使性能最高。
下面我們接著分析實例化線程的步驟:

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            // 使用模板方法newChild實例化一個線程
            children[i] = newChild(threadFactory, args);
            success = true;
        } catch (Exception e) {
            throw new IllegalStateException("failed to create a child event loop", e);
        } finally {
            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;
                    }
                }
            }
        }
    }

實現的過程一句話描述就是:使用newChild()依次實例化線程,如果出錯,關閉所有已經實例化的線程。也許你對finally中的代碼有疑問,這是因為不清楚shutdownGracefully()的含義。你需要提前明白這樣的事實:shutdownGracefully()只是通知線程池該關閉,但什么時候關閉由線程池決定,所以需要使用e.isTerminated()來判斷線程池是否真正關閉。
實例化線程池正常完成后,Netty使用下面的代碼設置異步終止結果:

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

分析完MultithreadEventExecutorGroup的構造方法,我們繼續分析普通方法。它的普通方法基本與下面的isTerminated()類似:

    @Override
    public boolean isTerminated() {
        for (EventExecutor l: children) {
            if (!l.isTerminated()) {
                return false;
            }
        }
        return true;
    }

總結起來就是:線程池的狀態由其中的各個線程決定。明白了這點,我們使用類比的方法可以推知其他方法的實現,故不再具體分析。

4.3.3 MultithreadEventLoopGroup

MultithreadEventLoopGroup實現了EventLoopGroup接口的方法,EventLoopGroup接口作為Netty并發的關鍵接口,我們看其中擴展的方法:

    // 將通道channel注冊到EventLoopGroup中的一個線程上
    ChannelFuture register(Channel channel);
    // 返回的ChannelFuture為傳入的ChannelPromise
    ChannelFuture register(Channel channel, ChannelPromise promise);
    // 覆蓋父類接口的方法,返回EventLoop
    @Override EventLoop next();

這些方法在MultithreadEventLoopGroup的具體實現很簡單。register()方法選擇一個線程,該線程負責具體的register()實現。next()方法使用父類實現,即使用上一節所述的選擇器選擇一個線程。代碼如下:

    @Override
    public ChannelFuture register(Channel channel) {
        return next().register(channel);
    }
    
    @Override
    public EventLoop next() {
        return (EventLoop) super.next();
    }

分析完這些代碼,我們關注一下線程數的默認設置。

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

默認情況,線程數最小為1,如果配置了系統參數io.netty.eventLoopThreads,設置為該系統參數值,否則設置為核心數的2倍。

4.3.4 NioEventLoopGroup

NioEventLoopGroup的主要代碼實現是模板方法newChild(),用來創建線程池中的單個線程,代碼如下:

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

關于代碼中的參數含義,我們放在NioEventLoop中分析。此外NioEventLoopGroup還提供了setIoRatio()和rebuildSelectors()兩個方法,一個用來設置I/O任務和非I/O任務的執行時間比,一個用來重建線程中的selector來規避JDK的epoll 100% CPU Bug。其實現也是依次設置各線程的狀態,故不再列出。

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

推薦閱讀更多精彩內容