Netty原始碼解析 -- 事件迴圈機制實現原理

binecy發表於2020-11-03

本文主要分享Netty中事件迴圈機制的實現。
原始碼分析基於Netty 4.1

EventLoop

前面分享服務端和客戶端啟動過程的文章中說過,Netty通過事件迴圈機制(EventLoop)處理IO事件和非同步任務,簡單來說,就是通過一個死迴圈,不斷處理當前已發生的IO事件和待處理的非同步任務。示例如下

while(true) {
	process(selector.select());

	process(getTask());
}

這種事件迴圈機制也是一種常用的IO事件處理機制,包括Redis,Mysql都使用了類似的機制。

關於非同步任務,前面文章說過,EventLoop實現了(jvm)Executor的介面,execute方法可以提供非同步任務。
register,bind,connect等操作,都會提交一個任務給EventLoop處理。如

if (eventLoop.inEventLoop()) {
	register0(promise);	
} else {
	eventLoop.execute(new Runnable() {	
		public void run() {
			register0(promise);
		}
	});
}

下面看一下Netty中事件迴圈機制相關的類。

EventExecutor,事件執行器,負責處理事件。
EventExecutorGroup維護了一個EventExecutor連結串列,它繼承了ScheduledExecutorService,execute方法通過next方法選擇一個EventExecutor,並呼叫EventLoop#execute處理事件。
(EventExecutor繼承了EventExecutorGroup,可以看做一個特殊的EventExecutorGroup,其execute方法可以提交一個任務任務)

EventLoop,事件迴圈器,繼承了EventExecutor,通過迴圈不斷處理註冊於其上的Channel的IO事件。
EventLoopGroup介面則繼承了EventExecutorGroup,負責排程EventLoop。

SingleThreadEventExecutor實現了EventExecutor,它會建立一個新執行緒,並在該執行緒上處理事件,可以理解為單執行緒處理器。
MultithreadEventExecutorGroup實現EventExecutorGroup,可以理解為多執行緒處理器(實際上是維護了多個EventExecutor,一個EventExecutor可以理解為一個執行緒),newChild方法構造具體的EventExecutor。
MultithreadEventExecutorGroup可以配置EventExecutor數量,即執行緒數量。
EventExecutorChooserFactory.EventExecutorChooser負責選擇一個EventExecutor執行實際操作。

NioEventLoop繼承了SingleThreadEventExecutor,負責處理NIO事件。所以,一個NioEventLoop物件可以看做是一個執行緒。
NioEventLoop也實現了EventLoop介面,它實現了事件迴圈機制,是Netty核心類。

MultithreadEventLoopGroup繼承了MultithreadEventExecutorGroup,並實現了EventLoopGroup,其newChild方法構造具體的EventLoop。
NioEventLoopGroup#newChild會構建NioEventLoop。

EventLoop各實現類關係如下

啟動

SingleThreadEventExecutor關鍵欄位

private final Queue<Runnable> taskQueue;	// 待處理非同步任務
private volatile Thread thread;				// EventLoop執行執行緒,即SingleThreadEventExecutor建立的新執行緒
private final Executor executor;			// java.util.concurrent.Executor,負責建立執行緒

當我們通過execute方法提交任務時,如果還沒有建立執行新執行緒,會通過SingleThreadEventExecutor#executor一個新執行緒,並在新執行緒中呼叫run方法(run方法由子類實現,負責實現事件迴圈機制,新執行緒是EventLoop真正執行執行緒)。

SingleThreadEventExecutor#execute

public void execute(Runnable task) {
	...

	boolean inEventLoop = inEventLoop();
	// #1
	addTask(task);
	// #2
	if (!inEventLoop) {
		startThread();
		// #3
		if (isShutdown()) {
			...
		}
	}
	// #4
	if (!addTaskWakesUp && wakesUpForTask(task)) {
		wakeup(inEventLoop);
	}
}

#1 新增任務到待處理列表
#2
inEventLoop方法,判斷當前執行緒是否為EventLoop執行執行緒
若當前執行緒非EventLoop執行執行緒,呼叫startThread方法啟動一個新的執行緒,執行run方法。
這裡可以理解為啟動EventLoop。
#3 如果當前EventLoop已關閉,拒絕任務
#4 若當前EventLoop執行緒阻塞正等待IO事件(Selector#select方法),呼叫wakeup方法喚醒執行緒執行該新增任務

迴圈機制

NioEventLoop#run方法負責實現NIO事件處理機制。

protected void run() {
	int selectCnt = 0;
	// #1
	for (;;) {

			int strategy;
			
				// #2
				strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
				switch (strategy) {
				case SelectStrategy.CONTINUE:
					continue;

				case SelectStrategy.BUSY_WAIT:
					// fall-through to SELECT since the busy-wait is not supported with NIO

				case SelectStrategy.SELECT:
					// #3
					long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
					if (curDeadlineNanos == -1L) {
						curDeadlineNanos = NONE; // nothing on the calendar
					}
					nextWakeupNanos.set(curDeadlineNanos);
					try {
						// #4
						if (!hasTasks()) {
							strategy = select(curDeadlineNanos);
						}
					} finally {
						// #5
						nextWakeupNanos.lazySet(AWAKE);
					}
					// fall through
				default:
				}
				...
			
			// #6
			selectCnt++;
			cancelledKeys = 0;
			needsToSelectAgain = false;
			final int ioRatio = this.ioRatio;
			boolean ranTasks;
			// #7
			if (ioRatio == 100) {
				try {
					if (strategy > 0) {
						processSelectedKeys();
					}
				} finally {
					// Ensure we always run tasks.
					ranTasks = runAllTasks();
				}
			} else if (strategy > 0) {
				final long ioStartTime = System.nanoTime();
				try {
					processSelectedKeys();
				} finally {
					// Ensure we always run tasks.
					final long ioTime = System.nanoTime() - ioStartTime;
					ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
				}
			} else {
				ranTasks = runAllTasks(0); // This will run the minimum number of tasks
			}
			// #8
			if (ranTasks || strategy > 0) {
				if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
					logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
							selectCnt - 1, selector);
				}
				selectCnt = 0;
			} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
				selectCnt = 0;
			}
		
		
			// #9
			if (isShuttingDown()) {
				closeAll();
				if (confirmShutdown()) {
					return;
				}
			}
		
	}
}

為了版面整潔,這裡刪除了異常處理程式碼。
#1 可以看到,這裡通過一個死迴圈不斷處理IO事件和非同步任務。
#2 如果當前存在待處理的任務,呼叫selector.selectNow(),這時會跳出switch語句,往下處理事件和任務,否則返回SelectStrategy.SELECT。
#3 curDeadlineNanos,計算延遲任務佇列中第一個任務的到期執行時間(即最晚還能延遲多長時間執行),沒有任務則返回-1。
更新nextWakeupNanos為阻塞時間。
由於頻繁呼叫(jvm)Selector.wakeup會造成效能消耗,NioEventLoop維護了一個喚醒標識nextWakeupNanos。nextWakeupNanos有三種值
NONE -- 執行執行緒被阻塞;
AWAKE -- 執行執行緒未阻塞;
其他值 -- 執行執行緒被超時阻塞,在指定的時間後喚醒;
NioEventLoop#wakeup方法中,只有nextWakeupNanos.getAndSet(AWAKE) != AWAKE成功才呼叫selector.wakeup()方法。
#4
這時如果還沒有任務加入,則執行select,阻塞執行緒。select方法返回結果作為新的strategy。
#5
lazySet方法,設定值之後其他執行緒在短期內還是可能讀到舊值
這裡將nextWakeupNanos設定為AWAKE,主要是減少wakeup方法中不必要的wakeup操作。
所以使用lazySet方法也沒有問題。
#6 selectCnt增加
舊版本的Java NIO在Linux Epoll實現上存在bug,(jvm)Selector.select方法可能在沒有任何就緒事件的情況下返回,導致CPU空轉,利用率飆升到100%。
於是,Netty計算select方法重複呼叫次數selectCnt,並在selectCnt大於SELECTOR_AUTO_REBUILD_THRESHOLD配置(預設512)時,重建selector,從而規避該問題。
幸好在JDK6_6u4,JDK7_b12已修復該Bug。
#7 processSelectedKeys方法處理IO事件,runAllTasks方法處理任務。
ioRatio表示執行IO事件所佔CPU時間百分比,預設50,
ioTime * (100 - ioRatio) / ioRatio,通過ioTime,ioRatio計算處理任務的CPU時間。
#8 如果執行了任務或者select方法返回有效值,直接重置selectCnt。
unexpectedSelectorWakeup方法中會在selectCnt大於SELECTOR_AUTO_REBUILD_THRESHOLD時重建selector。
#9 如果是正在關閉狀態,則要關閉所有的Channel

IO事件

下面看一下Eventloop中如何處理IO事件。
NioEventLoop關鍵欄位

Selector unwrappedSelector;				// JVM中的Selector
Selector selector;						// 優化後的SelectedSelectionKeySetSelector
SelectedSelectionKeySet selectedKeys;	// 對(jvm)Selector#selectedKeys進行優化

SelectedSelectionKeySetSelector每次呼叫select前都清除SelectedSelectionKeySet
SelectedSelectionKeySet使用陣列代替原Selector的中的HashSet,提高效能。陣列預設大小為1024,不夠用時擴充套件為原大小的2倍。

NioEventLoop#構造方法 -> NioEventLoop#openSelector

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

    if (DISABLE_KEY_SET_OPTIMIZATION) {
        return new SelectorTuple(unwrappedSelector);
    }

    ...

    final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
    final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

    Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
        
        public Object run() {
            try {
				// #2
                Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                ...

                selectedKeysField.set(unwrappedSelector, selectedKeySet);
                publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                return null;
            } ...
        }
    });

    ...
    selectedKeys = selectedKeySet;
    logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
	// #3
    return new SelectorTuple(unwrappedSelector,
                             new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}

#1 通過(jvm)SelectorProvider開啟一個Selector
#2 構造了selectedKeySet,並通過反射將該物件設定到Selector的selectedKeys,publicSelectedKeys屬性中,這樣Selector監聽到的事件就會儲存到selectedKeySet。
#3 構造了SelectedSelectionKeySetSelector物件

NioEventLoop#select負責阻塞執行緒,等待IO事件

private int select(long deadlineNanos) throws IOException {
	// #1
	if (deadlineNanos == NONE) {
		return selector.select();
	}

	// #2
	long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
	return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}

#1 一直阻塞,知道發生IO事件或加入了新任務
#2 計算阻塞時間,在原阻塞時間加上995微秒後轉化為毫秒。
如果原阻塞時間在5微秒內,就不阻塞了。

IO事件的處理流程為
NioEventLoop#processSelectedKeys -> (沒有禁用SelectedSelectionKeySet)processSelectedKeysOptimized -> processSelectedKey

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
	final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
	...

	try {
		int readyOps = k.readyOps();
		// #1
		if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
			int ops = k.interestOps();
			ops &= ~SelectionKey.OP_CONNECT;
			k.interestOps(ops);

			unsafe.finishConnect();
		}

		// #2
		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
		// #3
		if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
			unsafe.read();
		}
	} catch (CancelledKeyException ignored) {
		unsafe.close(unsafe.voidPromise());
	}
}

#1 處理OP_CONNECT
移除關注事件OP_CONNECT,否則Selector.select(..)將不斷返回
前面分享客戶端啟動過程的文章說過了,這裡會呼叫AbstractNioUnsafe#finishConnect,完成客戶端Connect操作,可回顧《客戶端啟動過程解析》。
#2 先處理OP_WRITE事件,能夠儘早寫入資料釋放記憶體,這裡涉及flush操作,後面有文章解析。
#3 處理OP_READ或OP_ACCEPT事件。
對於ServerChannel,這裡會呼叫NioMessageUnsafe#read,處理OP_ACCEPT事件,可回顧《客戶端啟動過程解析》。
對於SocketChannel,這裡會呼叫NioByteUnsafe#read,進行讀寫操作,後面有文章解析。

非同步任務

下面看一下Eventloop中如何處理非同步任務。
run方法#4步驟 -> SingleThreadEventExecutor#runAllTasks

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

    final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
    long runTasks = 0;
    long lastExecutionTime;
    for (;;) {
    	// #2
        safeExecute(task);

        runTasks ++;

        // #3
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }
        // #4
        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }
    // #5
    afterRunningAllTasks();
    this.lastExecutionTime = lastExecutionTime;
    return true;
}

#1 AbstractScheduledEventExecutor#scheduledTaskQueue中存放的是定時任務,
SingleThreadEventExecutor#taskQueue中存放的是待處理的任務。
fetchFromScheduledTaskQueue方法會獲取已到期的定時任務,移動到SingleThreadEventExecutor#taskQueue。
#2 執行獲取的任務
#3 每個64個任務檢查一次是否超時,因為nanoTime()方法也是一個相對昂貴的操作。
#4 取下一個任務,繼續處理
#5 預留的擴充套件方法。

EventLoop在4.1.44版本被優化,程式碼做了較大改動,刪除了原來的wakeup標誌,改用nextWakeupNanos,程式碼更清晰。
請參考 -- Clean up NioEventLoop

Netty是由事件驅動的,服務端register,bind,客戶端connect等操作都是提交非同步任務給EventLoop處理的
,而Accept,Read/Writ,Connect等IO事件都都需要EventLoop的處理。
大家可以結合前面分析服務端和客戶端啟動過程的文章,理解EventLoop是如何驅動Netty工作的。

如果您覺得本文不錯,歡迎關注我的微信公眾號。您的關注是我堅持的動力!

相關文章