作者: 一字馬胡
轉載標志 【2017-11-03】
更新日志
日期 | 更新內容 | 備注 |
---|---|---|
2017-11-03 | 添加轉載標志 | 持續更新 |
線程模型與并發
什么是線程模型呢?線程模型指定了線程管理的模型。在進行并發編程的過程中,我們需要小心的處理多個線程之間的同步關系,而一個好的線程模型可以大大減少管理多個線程的成本。在閱讀本文之前,你可以選擇性的閱讀下面列出的文章,來快速了解和回顧java中的并發編程內容:
- Java線程池詳解(一)
- Java線程池詳解(二)
- Java調度線程池ScheduleExecutorService
- Java調度線程池ScheduleExecutorService(續)
- Java中的ThreadLocal和 InheritableThreadLocal
- Java AQS
- Java可重入鎖詳解
Reactor線程模型
Reactor是一種經典的線程模型,Reactor線程模型分為單線程模型、多線程模型以及主從多線程模型。下面分別分析一下各個Reactor線程模型的優缺點。首先是Reactor單線程模型,下面的圖片展示了這個線程模型的結構:
Reactor單線程模型僅使用一個線程來處理所有的事情,包括客戶端的連接和到服務器的連接,以及所有連接產生的讀寫事件,這種線程模型需要使用異步非阻塞I/O,使得每一個操作都不會發生阻塞,Handler為具體的處理事件的處理器,而Acceptor為連接的接收者,作為服務端接收來自客戶端的鏈接請求。這樣的線程模型理論上可以僅僅使用一個線程就完成所有的事件處理,顯得線程的利用率非常高,而且因為只有一個線程在工作,所有不會產生在多線程環境下會發生的各種多線程之間的并發問題,架構簡單明了,線程模型的簡單性決定了線程管理工作的簡單性。但是這樣的線程模型存在很多不足,比如:
- 僅利用一個線程來處理事件,對于目前普遍多核心的機器來說太過浪費資源
- 一個線程同時處理N個連接,管理起來較為復雜,而且性能也無法得到保證,這是以線程管理的簡潔換取來的事件管理的復雜性,而且是在性能無 法得到保證的前提下換取的,在大流量的應用場景下根本沒有實用性
- 根據第二條,當處理的這個線程負載過重之后,處理速度會變慢,會有大量的事件堆積,甚至超時,而超時的情況下,客戶端往往會重新發送請求,這樣的情況下,這個單線程的模型就會成為整個系統的瓶頸
- 單線程模型的一個致命缺錢就是可靠性問題,因為僅有一個線程在工作,如果這個線程出錯了無法正常執行任務了,那么整個系統就會停止響應,也就是系統會因為這個單線程模型而變得不可用,這在絕大部分場景(所有)下是不允許出現的
介于上面的種種缺陷,Reactor演變出了第二種模型,也就是Reactor多線程模型,下面展示了這種模型:
可以發現,多線程模型下,接收鏈接和處理請求作為兩部分分離了,而Acceptor使用單獨的線程來接收請求,做好準備后就交給事件處理的handler來處理,而handler使用了一個線程池來實現,這個線程池可以使用Executor框架實現的線程池來實現,所以,一個連接會交給一個handler線程來復雜其上面的所有事件,需要注意,一個連接只會由一個線程來處理,而多個連接可能會由一個handler線程來處理,關鍵在于一個連接上的所有事件都只會由一個線程來處理,這樣的好處就是消除了不必要的并發同步的麻煩。Reactor多線程模型似乎已經可以很好的工作在我們的項目中了,但是還有一個問題沒有解決,那就是,多線程模型下任然只有一個線程來處理客戶端的連接請求,那如果這個線程掛了,那整個系統任然會變為不可用,而且,因為僅僅由一個線程來負責客戶端的連接請求,如果連接之后要做一些驗證之類復雜耗時操作再提交給handler線程來處理的話,就會出現性能問題。
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服務端啟動的代碼:
// 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的分配方式:
在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繼承了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的實現非常的復雜,但是只要我們清楚了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());
}
}
這個方法運行的流程為:
- 從Channel上獲取一個unsafe對象,這個對象 是用來進行NIO操作的一系列系統級API,關于Netty的Channel的深層次分析將在另外的篇章中進行
- 從Channel上獲取了eventLoop,而這個eventLoop是什么時候分配給Channel的細節在后文中進行分析
- 根據事件調用底層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 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的策略,對于本文涉及的內容的更為深入的分析總結,將在未來的某個適宜的時刻進行。