Netty原始碼解析 -- FastThreadLocal與HashedWheelTimer

binecy發表於2021-01-17

Netty原始碼分析系列文章已接近尾聲,本文再來分析Netty中兩個常見元件:FastThreadLoca與HashedWheelTimer。
原始碼分析基於Netty 4.1.52

FastThreadLocal

FastThreadLocal比較簡單。
FastThreadLocal和FastThreadLocalThread是配套使用的。
FastThreadLocalThread繼承了Thread,FastThreadLocalThread#threadLocalMap 是一個InternalThreadLocalMap,該InternalThreadLocalMap物件只能用於當前執行緒。
InternalThreadLocalMap#indexedVariables是一個陣列,存放了當前執行緒所有FastThreadLocal對應的值。
而每個FastThreadLocal都有一個index,用於定位InternalThreadLocalMap#indexedVariables。

FastThreadLocal#get

public final V get() {
    // #1
	InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
	// #2
	Object v = threadLocalMap.indexedVariable(index);
	if (v != InternalThreadLocalMap.UNSET) {
		return (V) v;
	}
    // #3
	return initialize(threadLocalMap);
}

#1 獲取該執行緒的InternalThreadLocalMap
如果是FastThreadLocalThread,直接獲取FastThreadLocalThread#threadLocalMap。
否則,從UnpaddedInternalThreadLocalMap.slowThreadLocalMap獲取該執行緒InternalThreadLocalMap。
注意,UnpaddedInternalThreadLocalMap.slowThreadLocalMap是一個ThreadLocal,這裡實際回退到使用ThreadLocal了。
#2 每個FastThreadLocal都有一個index。
通過該index,獲取InternalThreadLocalMap#indexedVariables中存放的值
#3 找不到值,通過initialize方法構建新物件。

可以看到,FastThreadLocal中連hash演算法都不用,通過下標獲取對應的值,複雜度為log(1),自然很快啦。

HashedWheelTimer

HashedWheelTimer是Netty提供的時間輪排程器。
時間輪是一種充分利用執行緒資源進行批量化任務排程的排程模型,能夠高效的管理各種延時任務。
簡單說,就是將延時任務存放到一個環形佇列中,並通過執行執行緒定時執行該佇列的任務。

例如,
環形佇列上有60個格子,
執行執行緒每秒移動一個格子,則環形佇列每輪可存放1分鐘內的任務。
現在有兩個定時任務
task1,32秒後執行
task2,2分25秒後執行
而執行執行緒當前位於第6格子
則task1放到32+6=38格,輪數為0
task2放到25+6=31個,輪數為2
執行執行緒將執行當前格子輪數為0的任務,並將其他任務輪數減1。

缺點,時間輪排程器的時間精度不高。
因為時間輪演算法的精度取決於執行執行緒移動速度。
例如上面例子中執行執行緒每秒移動一個格子,則排程精度小於一秒的任務就無法準時呼叫。

HashedWheelTimer關鍵欄位

// 任務執行器,負責執行任務
Worker worker = new Worker();
// 任務執行執行緒
Thread workerThread;
//  HashedWheelTimer狀態, 0 - init, 1 - started, 2 - shut down
int workerState;
// 時間輪佇列,使用陣列實現
HashedWheelBucket[] wheel;
// 暫存新增的任務
Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();
// 已取消任務
Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();

新增延遲任務 HashedWheelTimer#newTimeout

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
    ...

    // #1
    start();

    // #2
    long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

    ...
    HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
    timeouts.add(timeout);
    return timeout;
}

#1 如果HashedWheelTimer未啟動,則啟動該HashedWheelTimer
HashedWheelTimer#start方法負責是啟動workerThread執行緒
#2 startTime是HashedWheelTimer啟動時間
deadline是相對HashedWheelTimer啟動的延遲時間
構建HashedWheelTimeout,新增到HashedWheelTimer#timeouts

時間輪執行 Worker#run

public void run() {
	...

	// #1
	startTimeInitialized.countDown();

	do {
		// #2
		final long deadline = waitForNextTick();
		if (deadline > 0) {
			// #3
			int idx = (int) (tick & mask);
			processCancelledTasks();
			HashedWheelBucket bucket = wheel[idx];
			// #4
			transferTimeoutsToBuckets();
			// #5
			bucket.expireTimeouts(deadline);
			// #6
			tick++;
		}
	} while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

    // #7
	...
}

#1 HashedWheelTimer#start方法阻塞HashedWheelTimer執行緒直到Worker啟動完成,這裡解除HashedWheelTimer執行緒阻塞。
#2 計算下一格子開始執行的時間,然後sleep到下次格子開始執行時間
#2 tick是從HashedWheelTimer啟動後移動的總格子數,這裡獲取tick對應的格子索引。
由於Long型別足夠大,這裡並不考慮溢位問題。
#4 將HashedWheelTimer#timeouts的任務遷移到對應的格子中
#5 處理已到期任務
#6 移動到下一個格子
#7 這裡是HashedWheelTimer#stop後的邏輯處理,取消任務,停止時間輪

遷移任務 Worker#transferTimeoutsToBuckets

private void transferTimeoutsToBuckets() {
	// #1
	for (int i = 0; i < 100000; i++) {
		HashedWheelTimeout timeout = timeouts.poll();
		if (timeout == null) {
			// all processed
			break;
		}
		if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) {
			continue;
		}
		// #2
		long calculated = timeout.deadline / tickDuration;
		// #3
		timeout.remainingRounds = (calculated - tick) / wheel.length;
		// #4
		final long ticks = Math.max(calculated, tick); // Ensure we don't schedule for past.
		// #5
		int stopIndex = (int) (ticks & mask);

		HashedWheelBucket bucket = wheel[stopIndex];
		bucket.addTimeout(timeout);
	}
}

#1 注意,每次只遷移100000個任務,以免阻塞執行緒
#2 任務延遲時間/每格時間數, 得到該任務需延遲的總格子移動數
#3 (總格子移動數 - 已移動格子數)/每輪格子數,得到輪數
#4 如果任務在timeouts佇列放得太久導致已經過了執行時間,則使用當前tick, 也就是放到當前bucket,以便儘快執行該任務
#5 計算tick對應格子索引,放到對應的格子位置

執行到期任務 HashedWheelBucket#expireTimeouts

public void expireTimeouts(long deadline) {
	HashedWheelTimeout timeout = head;

	while (timeout != null) {
		HashedWheelTimeout next = timeout.next;
		// #1
		if (timeout.remainingRounds <= 0) {
			// #2
			next = remove(timeout);
			if (timeout.deadline <= deadline) {
				// #3
				timeout.expire();
			} else {
				throw new IllegalStateException(String.format(
						"timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
			}
		} else if (timeout.isCancelled()) {
			next = remove(timeout);
		} else {
			// #4
			timeout.remainingRounds --;
		}
		timeout = next;
	}
}

#1 選擇輪數小於等於0的任務
#2 移除任務
#3 修改狀態為過期,並執行任務
#4 其他任務輪數減1

ScheduledExecutorService使用堆(DelayedWorkQueue)維護任務,新增任務複雜度為O(logN)。
而 HashedWheelTimer 新增任務複雜度為O(1),所以在任務非常多時, HashedWheelTimer 可以表現出它的優勢。
但是任務較少甚至沒有任務時,HashedWheelTimer的執行執行緒都需要不斷移動,也會造成效能消耗。
注意,HashedWheelTimer使用同一個執行緒呼叫和執行任務,如果某些任務執行時間過久,則影響後續定時任務執行。當然,我們也可以考慮在任務中另起執行緒執行邏輯。
另外,如果任務過多,也會導致任務長期滯留在HashedWheelTimer#timeouts中而不能及時執行。

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

相關文章