Netty線程模型及EventLoop詳解

作者: 一字馬胡
轉載標志 【2017-11-03】

更新日志

日期 更新內容 備注
2017-11-03 添加轉載標志 持續更新

線程模型與并發

什么是線程模型呢?線程模型指定了線程管理的模型。在進行并發編程的過程中,我們需要小心的處理多個線程之間的同步關系,而一個好的線程模型可以大大減少管理多個線程的成本。在閱讀本文之前,你可以選擇性的閱讀下面列出的文章,來快速了解和回顧java中的并發編程內容:

Reactor線程模型

Reactor是一種經典的線程模型,Reactor線程模型分為單線程模型、多線程模型以及主從多線程模型。下面分別分析一下各個Reactor線程模型的優缺點。首先是Reactor單線程模型,下面的圖片展示了這個線程模型的結構:

Reactor單線程模型

Reactor單線程模型僅使用一個線程來處理所有的事情,包括客戶端的連接和到服務器的連接,以及所有連接產生的讀寫事件,這種線程模型需要使用異步非阻塞I/O,使得每一個操作都不會發生阻塞,Handler為具體的處理事件的處理器,而Acceptor為連接的接收者,作為服務端接收來自客戶端的鏈接請求。這樣的線程模型理論上可以僅僅使用一個線程就完成所有的事件處理,顯得線程的利用率非常高,而且因為只有一個線程在工作,所有不會產生在多線程環境下會發生的各種多線程之間的并發問題,架構簡單明了,線程模型的簡單性決定了線程管理工作的簡單性。但是這樣的線程模型存在很多不足,比如:

  • 僅利用一個線程來處理事件,對于目前普遍多核心的機器來說太過浪費資源
  • 一個線程同時處理N個連接,管理起來較為復雜,而且性能也無法得到保證,這是以線程管理的簡潔換取來的事件管理的復雜性,而且是在性能無 法得到保證的前提下換取的,在大流量的應用場景下根本沒有實用性
  • 根據第二條,當處理的這個線程負載過重之后,處理速度會變慢,會有大量的事件堆積,甚至超時,而超時的情況下,客戶端往往會重新發送請求,這樣的情況下,這個單線程的模型就會成為整個系統的瓶頸
  • 單線程模型的一個致命缺錢就是可靠性問題,因為僅有一個線程在工作,如果這個線程出錯了無法正常執行任務了,那么整個系統就會停止響應,也就是系統會因為這個單線程模型而變得不可用,這在絕大部分場景(所有)下是不允許出現的

介于上面的種種缺陷,Reactor演變出了第二種模型,也就是Reactor多線程模型,下面展示了這種模型:

Reactor多線程模型

可以發現,多線程模型下,接收鏈接和處理請求作為兩部分分離了,而Acceptor使用單獨的線程來接收請求,做好準備后就交給事件處理的handler來處理,而handler使用了一個線程池來實現,這個線程池可以使用Executor框架實現的線程池來實現,所以,一個連接會交給一個handler線程來復雜其上面的所有事件,需要注意,一個連接只會由一個線程來處理,而多個連接可能會由一個handler線程來處理,關鍵在于一個連接上的所有事件都只會由一個線程來處理,這樣的好處就是消除了不必要的并發同步的麻煩。Reactor多線程模型似乎已經可以很好的工作在我們的項目中了,但是還有一個問題沒有解決,那就是,多線程模型下任然只有一個線程來處理客戶端的連接請求,那如果這個線程掛了,那整個系統任然會變為不可用,而且,因為僅僅由一個線程來負責客戶端的連接請求,如果連接之后要做一些驗證之類復雜耗時操作再提交給handler線程來處理的話,就會出現性能問題。

Reactor多線程模型對Reactor單線程模型做了一些改進,但是在某些場景下任然有所缺陷,所以就有了第三種Reactor模型,Reactor主從多線程模型,下面展示了這種模型的架構:

Reactor主從多線程模型

Reactor多線程模型解決了Reactor單線程模型和Reactor多線程模型中存在的問題,解決了handler的性能問題,以及Acceptor的安全以及性能問題,Netty就使用了這種線程模型來處理事件。

Netty線程模型

在了解了線程模型以及Reactor線程模型之后,我們來看一下Netty的線程模型是怎么樣的。首先,Netty使用EventLoop來處理連接上的讀寫事件,而一個連接上的所有請求都保證在一個EventLoop中被處理,一個EventLoop中只有一個Thread,所以也就實現了一個連接上的所有事件只會在一個線程中被執行。一個EventLoopGroup包含多個EventLoop,可以把一個EventLoop當做是Reactor線程模型中的一個線程,而一個EventLoopGroup類似于一個ExecutorService,當然,這只是為了更好的理解Netty的線程模型,它們之間是沒有等價關系的,后面的分析中會詳細講到。下面的圖片展示了Netty的線程模型:

Netty線程模型

首先看一下Netty服務端啟動的代碼:


        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(your_handler_name, your_handler_instance);
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

Netty的服務端使用了兩個EventLoopGroup,而第一個EventLoopGroup通常只有一個EventLoop,通常叫做bossGroup,負責客戶端的連接請求,然后打開Channel,交給后面的EventLoopGroup中的一個EventLoop來負責這個Channel上的所有讀寫事件,一個Channel只會被一個EventLoop處理,而一個EventLoop可能會被分配給多個Channel來負責上面的事件,當然,Netty不僅支持NI/O,還支持OI/O,所以兩者的EventLoop分配方式有所區別,下面分別展示了NI/O和OI/O的分配方式:

Netty NIO分配EventLoop模型
Netty OIO分配EventLoop模型

在NI/O非阻塞模式下,Netty將負責為每個Channel分配一個EventLoop,一旦一個EventLoop唄分配給了一個Channel,那么在它的整個生命周期中都使用這個EventLoop,但是多個Channel將可能共享一個EventLoop,所以和Thread相關的ThreadLocal的使用就要特別注意,因為有多個Channel在使用該Thread來處理讀寫時間。在阻塞IO模式下,考慮到一個Channel將會阻塞,所以不太可能將一個EventLoop共用于多個Channel之間,所以,每一個Channel都將被分配一個EventLoop,并且反過來也成立,也就是一個EventLoop將只會被綁定到一個Channel上來處理這個Channel上的讀寫事件。無論是非阻塞模式還是阻塞模式,一個Channel都將會保證一個Channel上的所有讀寫事件都只會在一個EventLoop上被處理。

Netty EventLoop

上文中分析了Reactor線程模型以及Netty的線程模型,在Netty中,EventLoop是一個極為重要的組件,它翻譯過來稱為事件循環,一個EventLoop將被分配給一個Channel,來負責這個Channel的整個生命周期之內的所有事件,下面來分析一下EventLoop的結構和實現細節。首先展示了EventLoop的類圖:

EventLoop類圖

從EventLoop的類圖中可以發現,其實EventLoop繼承了Java的ScheduledExecutorService,也就是調度線程池,所以,EventLoop應當有ScheduledExecutorService提供的所有功能。那為什么需要繼承ScheduledExecutorService呢,也就是為什么需要延時調度功能,那是因為,在Netty中,有可能用戶線程和Netty的I/O線程同時操作網絡資源,而為了減少并發鎖競爭,Netty將用戶線程的任務包裝成Netty的task,然后向Netty的I/O任務一樣去執行它們。有些時候我們需要延時執行任務,或者周期性執行任務,那么就需要調度功能。這是Netty在設計上的考慮,為我們極大的簡化的編程方法。

EventLoop是一個接口,它在繼承了ScheduledExecutorService等多個類的同時,僅僅提供了一個方法parent,這個方法返回它屬于哪個EventLoopGroup。本文只分析非阻塞模式,而阻塞模式留到未來某個合適的時候再做分析總結。在上文中展示的服務端啟動的代碼中我們發現我們使用的EventLoop是一個子類NioEventLoopGroup,下面就來分析一下NioEventLoopGroup這個類。首先展示一下NioEventLoopGroup的類圖:

NioEventLoopGroup類圖

可以發現,NioEventLoopGroup的實現非常的復雜,但是只要我們清楚了Netty的線程模型,我們就可以有入口去分析它的代碼。首先,我們知道每個EventLoop只要一個Thread來處理事件,那我們就來找到那個Thread在什么地方。可以在SingleThreadEventExecutor類中找到thread,它的初始化在doStartThread這個方法中,而這個方法被startThread方法調用,而startThread 這個方法被execute方法調用,也就是提交任務的入口,這個方法是Executor接口的唯一方法。也就是說,所有我們通過EventLoop的execute方法提交的任務都將被這個Thread線程來執行。我們還知道一個事實,EventLoop是一個循環執行來消耗Channel事件的類,那么它必然會有一個類似循環的方法來作為任務,來提交給這個Thread來執行,而這可以在doStartThread方法中被發現,因為這個方法非常重要,所以下面展示了它的實現細節,但是去掉了一些代碼來減少代碼量:


    private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }

                boolean success = false;
                updateLastExecutionTime();
                try {
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    for (;;) {
                        int oldState = state;
                        if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                                SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                            break;
                        }
                    }

                    try {
                        // Run all remaining tasks and shutdown hooks.
                        for (;;) {
                            if (confirmShutdown()) {
                                break;
                            }
                        }
                    } finally {
                        try {
                            cleanup();
                        } finally {
                            STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                            threadLock.release();
                            terminationFuture.setSuccess(null);
                        }
                    }
                }
            }
        });
    }

上面所提到的事件循環就是通過SingleThreadEventExecutor.this.run()這句話來觸發的。這個run方法的具體實現在NioEventLoop中,下面展示了它的實現代碼:


    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();
                        }
                        // fall through
                    default:
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        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);
            }
        }
    }

首先,我們來分析一下NioEventLoop的相關細節,在一個無限循環里面,只有在遇到shutdown的情況下才會停止循環。然后在循環里會詢問是否有事件,如果沒有,則繼續循環,如果有事件,那么就開始處理時間。上文中我們提到,在事件循環中我們不僅要處理IO事件,還要處理非I/O事件。Netty中可以設置用于I/O操作和非I/O操作的時間占比,默認各位50%,也就是說,如果某次I/O操作的時間花了100ms,那么這次循環中非I/O得任務也可以花費100ms。Netty中的I/O時間處理通過processSelectedKeys方法來進行,而非I/O操作通過runAllTasks反復來進行,首先來看runAllTasks方法,雖然設定了一個可以運行的時間參數,但是實際上Netty并不保證能精確的確保非I/O任務只運行設定的毫秒,下面來看下runAllTasks的代碼:


    protected boolean runAllTasks(long timeoutNanos) {
        fetchFromScheduledTaskQueue();
        Runnable task = pollTask();
        if (task == null) {
            afterRunningAllTasks();
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            safeExecute(task);

            runTasks ++;

            // Check timeout every 64 tasks because nanoTime() is relatively expensive.
            // XXX: Hard-coded value - will make it configurable if it is really a problem.
            if ((runTasks & 0x3F) == 0) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }

            task = pollTask();
            if (task == null) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break;
            }
        }

        afterRunningAllTasks();
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

    // 將任務運行起來
    protected static void safeExecute(Runnable task) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception. Task: {}", task, t);
        }
    }

可以看到,這個方法是在每運行了64個任務之后再進行比較的,如果超出了設定的運行時間則退出,否則再運行64個任務再比較。所以,Netty強烈要求不要在I/O線程中運行阻塞任務,因為阻塞任務將會阻塞住Netty的事件循環,從而造成事件堆積的現象。現在回頭看處理I/O任務的processSelectedKeys方法,跟蹤代碼之后發現最后實際處理I/O事件的一個方法為processSelectedKey,下面展示了它的代碼:


    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop();
            } catch (Throwable ignored) {
                // If the channel implementation throws an exception because there is no event loop, we ignore this
                // because we are only trying to determine if ch is registered to this event loop and thus has authority
                // to close ch.
                return;
            }
            // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
            // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
            // still healthy and should not be closed.
            // See https://github.com/netty/netty/issues/5125
            if (eventLoop != this || eventLoop == null) {
                return;
            }
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
            return;
        }

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


這個方法運行的流程為:

  1. 從Channel上獲取一個unsafe對象,這個對象 是用來進行NIO操作的一系列系統級API,關于Netty的Channel的深層次分析將在另外的篇章中進行
  2. 從Channel上獲取了eventLoop,而這個eventLoop是什么時候分配給Channel的細節在后文中進行分析
  3. 根據事件調用底層API來處理事件

下面,我們分析一下是什么時候將一個EventLoop分配給一個Channel的,并且這個EventLoop的那個唯一的Thread是什么時候被賦值的。在這個問題上,服務端的流程和客戶端的流程可能不太一樣,對于服務端來說,首先需要bind一個端口,然后在進行Accept進來的連接,而客戶端需要進行connect到服務端。先來分析一下服務端。

還是看上面提供的服務端的示例代碼,其中啟動的代碼為下面這句代碼:


 // Start the server.
 ChannelFuture f = b.bind(PORT).sync();

也就是我們網絡編程中的bind操作,這個操作會發生什么呢?追蹤代碼如下:

 
 -> AbstractBootstrap.bind(port)
 -> AbstractBootstrap.bind(address) 
 -> AbstractBootstrap.doBind(final SocketAddress localAddress) 
 -> AbstractBootstrap.initAndRegister 
 -> AbstractBootstrap.doBind0 
 -> SingleThreadEventExecutor.execute 
 -> SingleThreadEventExecutor.startThread()
 -> SingleThreadEventExecutor.doStartThread

EventLoop在AbstractBootstrap.initAndRegister中獲得了一個新的Channel,然后在AbstractBootstrap.doBind0 方法里面調用接下來的方法來初始化EventLoop的Thread的工作,并且將EventLoop的時間循環打開了,可以開始接收客戶端的連接請求了。下面來分析一下客戶端的流程。

一個客戶端的啟動代碼示例:


        // Configure the client.
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoClientHandler());
                 }
             });

            // Start the client.
            ChannelFuture f = b.connect(HOST, PORT).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }

其中啟動的關鍵代碼為:


 // Start the client.
 ChannelFuture f = b.connect(HOST, PORT).sync();

下面是connect的調用流程:

 -> Bootstrap.doResolveAndConnect
 -> AbstractBootstrap.initAndRegister 
 -> Bootstrap.doResolveAndConnect0
 -> Bootstrap.doConnect
 -> SingleThreadEventExecutor.execute 
 -> SingleThreadEventExecutor.startThread()
 -> SingleThreadEventExecutor.doStartThread

后半部分和服務端的啟動過程是一致的,而區別在于服務端是通過bind操作來啟動的,而客戶端是通過connect操作來啟動的。執行到此,客戶端和服務端的EventLoop都已經啟動起來,服務端可以接受客戶端的連接并且處理Channel上的讀寫事件,而客戶端可以去連接遠程服務端來請求數據。

EventLoopGroup

到目前為止,我們已經知道了Reactor多個線程模型,并且知道了一個EventLoop會負責一個Channel的生命周期內的所有事件,并且知道了服務端和客戶端是如何啟動這個EventLoop得,但是還有一個問題沒有解決,那就是一個EventLoop是如何被分配給一個Channel的。下文就來分析這個分配的原理和過程。而對于阻塞I/O模型的分配和非阻塞I/O模型的分配是不一樣的,在上文中也提到這個內容,所以本文只分析對于非阻塞I/O模型的分配。

EventLoopGroup是用來管理EventLoop的對象,一個EventLoopGroup里面有多個EventLoop,下面展示了EventLoopGroup的類圖:

EventLoopGroup類圖

我們從實際的代碼出發來分析EventLoopGroup。上文中已經展示了客戶端和服務端的啟動代碼,其中有類似的代碼如下:


        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

上文中我們分析了EventLoop被啟動的過程,我們肯定,EventLoop是在分配之后啟動的,因為對于服務端而言,bind是一個最開始的網絡操作,對于客戶端來說,connect也是最開始的網絡操作,在這之前是沒有關于網絡I/O的操作的,所以,EventLoop的分配和啟動是在這兩個過程或者之后的流程中進行的,但是EventLoop的分配肯定是在啟動之前的,但是EventLoop的分配和啟動在bind和connect中進行,那么我們可以肯定,EventLoop的分配也是在這兩個方法中進行的。為了證明這個假設,回頭再看一下服務端的EventLoop的啟動過程,其中有一個方法值得我們注意:AbstractBootstrap.initAndRegister,我們進行了init部分的分析,而register部分我們還沒有分析,下面就對服務端來進行register部分的分析,下面展示了register的調用鏈路:


 -> Bootstrap.doResolveAndConnect
 -> AbstractBootstrap.initAndRegister 
 -> EventLoopGroup.register
 -> MultithreadEventLoopGroup.register
 -> SingleThreadEventLoop.register
 -> Channel.register
 -> AbstractUnsafe.register
 
         public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            if (eventLoop == null) {
                throw new NullPointerException("eventLoop");
            }
            if (isRegistered()) {
                promise.setFailure(new IllegalStateException("registered to an event loop already"));
                return;
            }
            if (!isCompatible(eventLoop)) {
                promise.setFailure(
                        new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
                return;
            }

            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
        }

最后展示了AbstractUnsafe.register這個方法,在這里初始化了一個EventLoop,需要記住的一點是,EventLoopGroup中的是EventLoop,不然在追蹤代碼的時候會迷失。現在來正式看一下NioEventLoopGroup這個類,它的它繼承了MultithreadEventExecutorGroup這個類,而我們在初始化EventLoopGroup的時候傳遞進去的參數,也就是我們希望這個EventLoopGroup擁有的EventLoop數量,會在MultithreadEventExecutorGroup這個類中初始化,并且是在構造函數中初始化的,如果在new EventLoopGroup的時候沒有任何參數,那么默認的EventLoop的數量是機器CPU數量的兩倍。現在我們來看一下MultithreadEventExecutorGroup這個類的一個重要的構造函數,這個構造函數初始化了EventLoopGroup的EventLoop。


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

        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }

        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, 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 {
                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) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }

        chooser = chooserFactory.newChooser(children);

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

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

一個較為重要的方法為newChild,這是初始化一個EventLoop的方法,下面是它的具體實現,假設我們使用NioEventLoop:


    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }

我們現在知道了EventLoopGroup管理著很多的EventLoop,上文中我們僅僅分析了分配的流程,但是分配的策略還沒有分析,現在來分析一下EventLoopGroup是如何分配EventLoop給Channel的,我們僅分析非阻塞I/O下的分配策略,阻塞模式下的分配策略可以參考非阻塞下的分配策略。

在MultithreadEventLoopGroup.register方法中,調用了next()方法,我們來看一下這個流程:


  -> MultithreadEventExecutorGroup.next()
  
    public EventExecutor next() {
        return chooser.next();
    }

chooser是什么東西?


 private final EventExecutorChooserFactory.EventExecutorChooser chooser;

它是怎么初始化的呢?


    public EventExecutorChooser newChooser(EventExecutor[] executors) {
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }

這是它初始化最后調用的方法,這個方法在DefaultEventExecutorChooserFactory中被實現,這個參數是MultithreadEventExecutorGroup類中的children,也就是EventLoopGroup中的所有EventLoop,那這個newChooser得分配方法就是如果EventLoop的數量是2的n次方,那么就使用PowerOfTwoEventExecutorChooser來分配,否則使用GenericEventExecutorChooser來分配。這兩個策略類的分配方法實現分別如下:

     
     1、PowerOfTwoEventExecutorChooser
        public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }

    2、GenericEventExecutorChooser
        public EventExecutor next() {
            return executors[Math.abs(idx.getAndIncrement() % executors.length)];
        }

所以,到此為止,我們可以解決為什么一個EventLoop會被分配給多個Channel的疑惑。本文到此也就結束了。篇幅較長,內容涉及到Reactor的三種線程模型,然后分析了Netty的線程模型,然后分析了Netty的EventLoop,以及EventLoopGroup,以及分析了EventLoop是怎么被分配給一個Channel的,和一個EventLoop是如何啟動起來來處理事件的。最后分析了EventLoopGroup分配EventLoop的策略,對于本文涉及的內容的更為深入的分析總結,將在未來的某個適宜的時刻進行。

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

推薦閱讀更多精彩內容