2.1 多執行緒程式設計基礎
為了更平穩地過渡,在真正進入UE的多執行緒渲染知識之前,先學習或重溫一下多執行緒程式設計的基礎知識。
2.1.1 多執行緒概述
多執行緒(Multithread)程式設計的思想早在單核時代就已經出現了,當時的作業系統(如Windows95)就已經支援多工的功能,其原理就是在單核中切換不同的上下文(Context),以便每個程式中的執行緒都有時間獲得執行指令的機會。
但到了2005年,當單核主頻接近4GHz時,CPU硬體廠商英特爾和AMD發現,速度也會遇到自己的極限:那就是單純的主頻提升,已經無法明顯提升系統整體效能。
隨著單核計算頻率摩爾定律的緩慢終結,Intel率先於2005年釋出了奔騰D和奔騰四至尊版840系列,首次支援了兩個物理級別的執行緒計算單元。此後十多年,多核CPU得到蓬勃發展,由AMD製造的Ryzen 3990X處理器已經擁有64個核心128個邏輯執行緒。
銳龍(Ryzen)3990X的宣傳海報中赫然凸顯的核心與執行緒數量。
硬體的多核發展,給軟體極大的發揮空間。應用程式可以充分發揮多核多執行緒的計算資源,各個應用領域由此也產生多執行緒程式設計模型和技術。作為遊戲的發動機Unreal Engine等商業引擎,同樣可以利用多執行緒技術,以便更加充分地提升效率和效果。
使用多執行緒併發帶來的作用總結起來主要有兩點:
- 分離關注點。通過將相關的程式碼與無關的程式碼分離,可以使程式更容易理解和測試,從而減少出錯的可能性。比如,遊戲引擎中通常將檔案載入、網路傳輸放入獨立的執行緒中,既可以不阻礙主執行緒,也可以分離邏輯程式碼,使得更加清晰可擴充套件。
- 提升效能。人多力量大,這樣的道理同樣用到CPU上(核多力量大)。相同量級的任務,如果能夠分散到多個CPU中同時執行,必然會帶來效率的提升。
但是,隨著CPU核心數量的提升,計算機獲得的效益並非直線提升,而是遵循Amdahl's law(阿姆達爾定律),Amdahl's law的公式定義如下:
公式的各個分量含義如下:
- \(S_{latency}\):整個任務在多執行緒處理中理論上獲得的加速比。
- \(s\):用於執行任務並行部分的硬體資源的執行緒數量。
- \(p\):可並行處理的任務佔比。
舉個具體的栗子,假設有8核16執行緒的CPU用於處理某個任務,這個任務有70%的部分是可以並行處理的,那麼它的理論加速比為:
由此可見,多執行緒程式設計帶來的效益並非跟核心數呈直線正比,實際上它的曲線如下所示:
阿姆達爾定律揭示的核心數和加速比圖例。由此可見,可並行的任務佔比越低,加速比獲得的效果越差:當可並行任務佔比為50%時,16核已經基本達到加速比天花板,無論後面增加多少核心數量,都無濟於事;如果可並行任務佔比為95%時,到2048個核心才會達到加速比天花板。
雖然阿姆達爾定律給我們帶來了殘酷的現實,但是,如果我們能夠提升任務並行佔比到接近100%,則加速比天花板可以得到極大提升:
如上公式所示,當\(p=1\)(即可並行的任務佔比100%)時,理論上的加速比和核心數量成線性正比!!
舉個具體的例子,在編譯Unreal Engine工程原始碼或Shader時,由於它們基本是100%的並行佔比,理論上可以獲得接近線性關係的加速比,在多核系統中將極大地縮短編譯時間。
利用多執行緒併發提高效能的方式有兩種:
- 任務並行(task parallelism)。將一個單個任務分成幾部分,且各自並行執行,從而降低總執行時間。這種方式雖然看起來很簡單直觀,但實際操作中可能會很複雜,因為在各個部分之間可能存在著依賴。
- 資料並行(data parallelism)。任務並行的是演算法(執行指令)部分,即每個執行緒執行的指令不一樣;而資料並行是指令相同,但執行的資料不一樣。SIMD也是資料並行的一種方式。
上面闡述了多執行緒併發的益處,接下來說說它的副作用。總結起來,副作用如下:
- 導致資料競爭。多執行緒訪問常常會交叉執行同一段程式碼,或者操作同一個資源,又或者多核CPU的高度快取同步問題,由此變化帶來各種資料不同步或資料讀寫錯誤,由此產生了各種各樣的異常結果,這便是資料競爭。
- 邏輯複雜化,難以除錯。由於多執行緒的併發方式不唯一,不可預知,所以為了避免資料競爭,常常加入複雜多樣的同步操作,程式碼也會變得離散、片段化、繁瑣、難以理解,增加程式碼的輔助,對後續的維護、擴充套件都帶來不可估量的阻礙。也會引發小概率事件難以重現的BUG,給除錯和查錯增加了數量級的難度。
- 不一定能夠提升效益。多執行緒技術用得到確實會帶來效率的提升,但並非絕對,常和物理核心、同步機制、執行時狀態、併發佔比等等因素相關,在某些極端情況,或者用得不夠妥當,可能反而會降低程式效率。
2.1.2 多執行緒概念
本小節將闡述多執行緒程式設計技術中常涉及的基本概念。
- 程式(Process)
程式(Process)是作業系統執行應用程式的基本單元和實體,它本身只是個容器,通常包含核心物件、地址空間、統計資訊和若干執行緒。它本身並不真正執行程式碼指令,而是交由程式內的執行緒執行。
對Windows而言,作業系統在建立程式時,同時也會給它建立一個執行緒,該執行緒被稱為主執行緒(Primary thread, Main thread)。
對Unix而言,程式和主執行緒其實是同一個東西,作業系統並不知道有執行緒的存在,執行緒更接近於lightweight processes(輕量級程式)的概念。
程式有優先順序概念,Windows下由低到高為:低(Low)、低於正常(Below normal)、正常(Normal)、高於正常(Above normal)、高(High)、實時(Real time)。(見下圖)
預設情況下,程式的優先順序為Normal。優先順序高的程式將會優先獲得執行機會和時間。
- 執行緒(Thread)
執行緒(Thread)是可以執行程式碼的實體,通常不能獨立存在,需要依附在某個程式內部。一個程式可以擁有多個執行緒,這些執行緒可以共享程式的資料,以便並行或併發地執行多個任務。
在單核CPU中,作業系統(如Windows)可能會採用輪循(Round robin)的方式進行排程,使得多個執行緒看起來是同時執行的。(下圖)
在多核CPU中,執行緒可能會安排在不同的CPU核心同時執行,從而達到並行處理的目的。
採用SMP的Windows在多核CPU的執行示意圖。等待處理的執行緒被安排到不同的CPU核心。
每個執行緒可擁有自己的執行指令上下文(如Windows的IP(指令暫存器地址)和SP(棧起始暫存器地址))、執行棧和TLS(Thread Local Storage,執行緒區域性快取)。
Windows執行緒建立和初始化示意圖。
執行緒區域性儲存(Thread Local Storage)是一種儲存持續期,物件的生命週期與執行緒一樣,線上程開始時分配,執行緒結束時回收。每個執行緒有該物件自己的例項,訪問和修改這樣的物件不會造成競爭條件(Race Condition)。
執行緒也存在優先順序概念,優先順序越高的將優先獲得執行指令的機會。
執行緒的狀態一般有執行狀態、暫停狀態等。Windows可用以下介面切換執行緒狀態:
// 暫停執行緒
DWORD SuspendThread(HANDLE hThread);
// 繼續執行執行緒
DWORD ResumeThread(HANDLE hThread);
同個執行緒可被多次暫停,如果要恢復執行狀態,則需要呼叫同等次數的繼續執行介面。
- 協程(Coroutine)
協程(Coroutine)是一種輕量級(lightweight)的使用者態執行緒,通常跑在同一個執行緒,利用同一個執行緒的不同時間片段執行指令,沒有執行緒、程式切換和排程的開銷。從使用者角度,可以利用協程機制實現在同個執行緒模擬非同步的任務和編碼方式。在同個執行緒內,它不會造成資料競爭,但也會因執行緒阻塞而阻塞。
- 纖程(Fiber)
纖程(Fiber)如同協程,也是一種輕量級的使用者態執行緒,可以使得應用程式獨立決定自己的執行緒要如何運作。作業系統核心不知道纖程的存在,也不會為它進行排程。
- 競爭條件(Race Condition)
同個程式允許有多個執行緒,這些執行緒可以共享程式的地址空間、資料結構和上下文。程式內的同一資料塊,可能存在多個執行緒在某個很小的時間片段內同時讀寫,這就會造成資料異常,從而導致了不可預料的結果。這種不可預期性便造就了競爭條件(Race Condition)。
避免產生競爭條件的技術有很多,諸如原子操作、臨界區、讀寫鎖、核心物件、訊號量、互斥體、柵欄、屏障、事件等等。
- 並行(Parallelism)
至少兩個執行緒同時執行任務的機制。一般有多核多物理執行緒的CPU同時執行的行為,才可以叫並行,單核的多執行緒不能稱之為並行。
- 併發(Concurrency)
至少兩個執行緒利用時間片(Timeslice)執行任務的機制,是並行的更普遍形式。即便單核CPU同時執行的多執行緒,也可稱為併發。
併發的兩種形式——上:雙物理核心的同時執行(並行);下:單核的多工切換(併發)。
事實上,併發和並行在多核處理器中是可以同時存在的,比如下圖所示,存在雙核,每個核心又同時切換著多個任務:
部分參考文獻嚴格區分了並行和併發,但部分文獻並不明確指出其中的區別。虛幻引擎的多執行緒渲染架構和API中,常出現並行和併發的概念,所以虛幻是明顯區分兩者之間的含義。
- 執行緒池(Thread Pool)
執行緒池提供了一種新的任務併發的方式,呼叫者只需要傳入一組可並行的任務和分組的策略,便可以使用執行緒池的若干執行緒併發地執行任務,使得呼叫者無需接直接觸執行緒的呼叫和管理細節,降低了呼叫者的成本,也提升了執行緒的排程效率和吞吐量。
不過,建立一個執行緒池時,幾個關鍵性的設計問題會影響併發效率,比如:可使用的執行緒數量,高效的任務分配方式,以及是否需要等待一個任務完成。
執行緒池可以自定義實現,也可以直接使用C++、作業系統或第三方庫提供的API。
2.1.3 C++的多執行緒
在C++11之前,C++的多執行緒支援基本為零,僅提供少量雞肋的volatile
等關鍵字。直到C++11標準,多執行緒才真正納入C++標準,並提供了相關關鍵字、STL標準庫,以便使用者實現跨平臺的多執行緒呼叫。
當然,對使用者來說,多執行緒的實現可採用C++11的執行緒庫,也可以根據具體的系統平臺提供的多執行緒API自定義執行緒庫,還可以使用諸如ACE、boost::thread等第三方庫。使用C++自帶的多執行緒庫,有幾個優點,一是使用簡單方便,依賴少;二是跨平臺,無需關注系統底層。
2.1.3.1 C++多執行緒關鍵字
- thread_local
thread_local是C++是實現執行緒區域性儲存的關鍵,新增了此關鍵字的變數意味著每個執行緒都有自己的一份資料,不會共享同一份資料,避免資料競爭。
C11的關鍵字_Thread_local
用於定義執行緒區域性變數。在標頭檔案<threads.h>
定義了thread_local
為上述關鍵詞的同義。例如:
#include <threads.h>
thread_local int foo = 0;
C++11引入的thread_local
關鍵字用於下述情形:
1、名字空間(全域性)變數。
2、檔案靜態變數。
3、函式靜態變數。
4、靜態成員變數。
此外,不同編譯器提供了各自的方法宣告執行緒區域性變數:
// Visual C++, Intel C/C++ (Windows systems), C++Builder, Digital Mars C++
__declspec(thread) int number;
// Solaris Studio C/C++, IBM XL C/C++, GNU C, Clang, Intel C++ Compiler (Linux systems)
__thread int number;
// C++ Builder
int __thread number;
- volatile
使用了volatile修飾符的變數意味著它在記憶體中的值可能隨時發生變化,也告訴編譯器不能做任何優化,每次使用到此變數的值都必須從記憶體中讀取,而不應該直接使用暫存器的值。
舉個具體的栗子吧。假設有以下程式碼段:
int a = 10;
volatile int *p = &a;
int b, c;
b = *p;
c = *p;
若p
沒有volatile
修飾,則b = *p
和c = *p
只需從記憶體取一次p
的值,那麼b
和c
的值必然是10
。
若考慮volatile
的影響,假設執行完b = *p
語句之後,p
的值被其它執行緒修改了,則執行c = *p
會再次從記憶體中讀取p
的值,此時c
的值不再是10,而是新的值。
但是,volatile並不能解決多執行緒的同步問題,只適合以下三種情況使用:
1、和訊號處理(signal handler)相關的場合。
2、和記憶體對映硬體(memory mapped hardware)相關的場合。
3、和非本地跳轉(setjmp
和 longjmp
)相關的場合。
- std::atomic
嚴格來說atomic
並不是關鍵字,而是STL的模板類,可以支援指定型別的原子操作。
使用原子的型別意味著該型別的例項的讀寫操作都是原子性的,無法被其它執行緒切割,從而達到執行緒安全和同步的目標。
可能有些讀者會好奇,為什麼對於基本型別的操作也需要原子操作。比如:
int cnt = 0;
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f};
以上三個執行緒同時呼叫函式f
,該函式只執行cnt++
,在C++維度,似乎只有一條執行語句,理論上不應該存在同步問題。然而,編譯成彙編指令後,會有多條指令,這就會在多執行緒中引起執行緒上下文切換,引起不可預知的行為。
為了避免這種情況,就需要加入atomic
型別:
std::atomic<int> cnt{0}; // 給cnt加入原子操作。
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f};
加入atomic
之後,所有執行緒執行後的結果是確定的,能夠正常給變數計數。atomic
的實現機制與臨界區類似,但效率上比臨界區更快。
為了更進一步地說明C++的單條語句可能生成多條彙編指令,可藉助Compiler Explorer來實時查探C++彙編後的指令:
Compiler Explorer動態將左側C++語句編譯出的彙編指令。上圖所示的c++程式碼編譯後可能存在一對多的彙編指令,由此印證atomic原子操作的必要性。
充分利用std::atomic
的特性和介面,可以實現很多非阻塞無鎖的執行緒安全的資料結構和演算法,關於這一點的延伸閱讀,強力推薦《C++ Concurrency In Action》。
2.1.3.2 C++執行緒
C++的執行緒型別是std::thread
,它提供的介面如下表:
介面 | 解析 |
---|---|
join | 加入主執行緒,使得主執行緒強制等待該執行緒執行完。 |
detach | 從主執行緒分離,使得主執行緒無需等待該執行緒執行完。 |
swap | 與另外一個執行緒交換執行緒物件。 |
joinable | 查詢是否可加入主執行緒。 |
get_id | 獲取該執行緒的唯一識別符號。 |
native_handle | 返回實現層的執行緒控制程式碼。 |
hardware_concurrency | 靜態介面,返回硬體支援的併發執行緒數量。 |
使用範例:
#include <iostream>
#include <thread>
#include <chrono>
void foo()
{
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
std::cout << "starting thread...\n";
std::thread t(foo); // 構造執行緒物件,且傳入被執行的函式。
std::cout << "waiting for thread to finish..." << std::endl;
t.join(); // 加入主執行緒,使得主執行緒必須等待該執行緒執行完畢。
std::cout << "done!\n";
}
輸出:
starting thread...
waiting for thread to finish...
done!
如果需要在呼叫執行緒和新執行緒之間同步資料,則可以使用C++的std::promise
和std::future
等機制。示例程式碼:
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // Notify future
}
int main()
{
// Demonstrate using promise<int> to transmit a result between threads.
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
// future::get() will wait until the future has a valid result and retrieves it.
// Calling wait() before get() is not needed
//accumulate_future.wait(); // wait for result
std::cout << "result = " << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
}
輸出結果:
result = 21
但是,std::thread
的執行並不能保證是非同步的,也可能是在當前執行緒執行。
如果需要強制非同步,則可使用std::async
。它可以指定兩種非同步方式:std::launch::async
和std::launch::deferred
,前者表示使用新的執行緒非同步地執行任務,後者表示在當前執行緒執行,且會被延遲執行。使用範例:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>
std::mutex m;
struct X {
void foo(int i, const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << ' ' << i << '\n';
}
void bar(const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << '\n';
}
int operator()(int i) {
std::lock_guard<std::mutex> lk(m);
std::cout << i << '\n';
return i + 10;
}
};
template <typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
auto len = end - beg;
if (len < 1000)
return std::accumulate(beg, end, 0);
RandomIt mid = beg + len/2;
auto handle = std::async(std::launch::async,
parallel_sum<RandomIt>, mid, end);
int sum = parallel_sum(beg, mid);
return sum + handle.get();
}
int main()
{
std::vector<int> v(10000, 1);
std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
X x;
// Calls (&x)->foo(42, "Hello") with default policy:
// may print "Hello 42" concurrently or defer execution
auto a1 = std::async(&X::foo, &x, 42, "Hello");
// Calls x.bar("world!") with deferred policy
// prints "world!" when a2.get() or a2.wait() is called
auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
// Calls X()(43); with async policy
// prints "43" concurrently
auto a3 = std::async(std::launch::async, X(), 43);
a2.wait(); // prints "world!"
std::cout << a3.get() << '\n'; // prints "53"
} // if a1 is not done at this point, destructor of a1 prints "Hello 42" here
執行結果:
The sum is 10000
43
Hello 42
world!
53
另外,C++20已經支援輕量級的協程(coroutine)了,相關的關鍵字:co_await
,co_return
,co_yield
,跟C#等指令碼語言的概念和用法如出一轍,但行為和實現機制可能會稍有不同,此文不展開探討了。
2.1.3.3 C++多執行緒同步
執行緒同步的機制有很多,C++支援的有以下幾種:
- std::atomic
[2.1.3.1 C++多執行緒關鍵字](#2.1.3.1 C++多執行緒關鍵字)已經對std::atomic
做了詳細的解析,可以防止多執行緒之間共享資料的資料競險問題。此外,它還提供了豐富多樣的介面和狀態查詢,以便更加精細和高效地同步原子資料,常見介面和解析如下:
介面名 | 解析 |
---|---|
is_lock_free | 檢查原子物件是否無鎖的。 |
store | 儲存值到原子物件。 |
load | 從原子物件載入值。 |
exchange | 獲取原子物件的值,並替換成指定值。 |
compare_exchange_weak, compare_exchange_strong | 將原子物件的值和預期值(expected)對比,如果相同就替換成目標值(desired),並返回true ;如果不同,就載入原子物件的值到預期值(expected),並返回false 。weak模式不會卡呼叫執行緒,strong模式會卡住呼叫執行緒,直到原子物件的值和預期值(expected)相同。 |
fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor | 獲取原子物件的值,並對其相加、相減等操作。 |
operator ++, operator --, operator +=, operator -=, ... | 對原子物件響應各類操作符,操作符的意義和普通變數一致。 |
此外,C++20還支援wait, notify_one, notify_all等同步介面。
利用compare_exchange_weak
介面可以很方便地實現執行緒安全的非阻塞式的資料結構。示例:
#include <atomic>
#include <future>
#include <iostream>
template<typename T>
struct node
{
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
};
template<typename T>
class stack
{
public:
std::atomic<node<T>*> head; // 堆疊頭, 採用原子操作.
public:
// 入棧操作
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// 將原有的頭指標作為新節點的下一節點.
new_node->next = head.load(std::memory_order_relaxed);
// 將新的節點和老的頭部節點做對比測試, 如果new_node->next==head, 說明其它執行緒沒有修改head, 可以將head替換成new_node, 從而完成push操作.
// 反之, 如果new_node->next!=head, 說明其它執行緒修改了head, 將其它執行緒修改的head儲存到new_node->next, 繼續迴圈檢測.
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // 空迴圈體
}
};
int main()
{
stack<int> s;
auto r1 = std::async(std::launch::async, &stack<int>::push, &s, 1);
auto r2 = std::async(std::launch::async, &stack<int>::push, &s, 2);
auto r3 = std::async(std::launch::async, &stack<int>::push, &s, 3);
r1.wait();
r2.wait();
r3.wait();
// print the stack's values
node<int>* node = s.head.load(std::memory_order_relaxed);
while(node)
{
std::cout << node->data << " ";
node = node->next;
}
}
輸出:
2 3 1
由此可見,利用原子及其介面可以很方便地進行多執行緒同步,而且由於是多執行緒非同步入棧,棧的元素不一定與編碼的順序一致。
以上程式碼還涉及記憶體訪問順序的標記:
- 排序一致序列(sequentially consistent)。
- 獲取-釋放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel)。
- 自由序列(memory_order_relaxed)。
關於這方面的詳情可以參看第一篇的記憶體屏障或者《C++ concurrency in action》的章節5.3 同步操作和強制排序。
- std::mutex
std::mutex即互斥量,它會在作用範圍內進入臨界區(Critical section),使得該程式碼片段同時只能由一個執行緒訪問,當其它執行緒嘗試執行該片段時,會被阻塞。std::mutex常與std::lock_guard
,示例程式碼:
#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex; // 宣告互斥量
void save_page(const std::string &url)
{
// simulate a long page fetch
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string result = "fake content";
// 配合std::lock_guard使用, 可以及時進入和釋放互斥量.
std::lock_guard<std::mutex> guard(g_pages_mutex);
g_pages[url] = result;
}
int main()
{
std::thread t1(save_page, "http://foo");
std::thread t2(save_page, "http://bar");
t1.join();
t2.join();
// safe to access g_pages without lock now, as the threads are joined
for (const auto &pair : g_pages) {
std::cout << pair.first << " => " << pair.second << '\n';
}
}
輸出:
http://bar => fake content
http://foo => fake content
此外,手動操作std::mutex
的鎖定和解鎖,可以實現一些特殊行為,例如等待某個標記:
#include <chrono>
#include <thread>
#include <mutex>
bool flag;
std::mutex m;
void wait_for_flag()
{
std::unique_lock<std::mutex> lk(m); // 這裡採用std::unique_lock而非std::lock_guard. std::unique_lock可以實現嘗試獲得鎖, 如果當前以及被其它執行緒鎖定, 則延遲直到其它執行緒釋放, 然後才獲得鎖.
while(!flag)
{
lk.unlock(); // 解鎖互斥量
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 休眠100ms,在此期間,其它執行緒可以進入互斥量,以便更改flag標記。
lk.lock(); // 再鎖互斥量
}
}
- std::condition_variable
std::condition_variable
和std::condition_variable_any
都是條件變數,都是C++標準庫的實現,它們都需要與互斥量配合使用。由於std::condition_variable_any
更加通用,會在效能上產生更多的開銷。故而,應當首先考慮使用std::condition_variable
。
利用條件變數的介面,結合互斥量的使用,可以很方便地執行執行緒間的等待、通知等操作。示例:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv; // 宣告條件變數
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 等待直到主執行緒改變ready為true.
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// 獲得了互斥量的鎖
std::cout << "Worker thread is processing data\n";
data += " after processing";
// 傳送資料給主執行緒
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// 手動解鎖, 以便主執行緒獲得鎖.
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// send data to the worker thread
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// wait for the worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
輸出:
main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing
- std::future
C++的future(期望)是一種可以訪問未來的返回值的機制,常用於多執行緒的同步。可以建立future的型別有: std::async, std::packaged_task, std::promise。
future物件可以執行wait、wait_for、wait_until,從而實現事件等待和同步,示例程式碼:
#include <iostream>
#include <future>
#include <thread>
int main()
{
// 從packaged_task獲取的future
std::packaged_task<int()> task([]{ return 7; }); // wrap the function
std::future<int> f1 = task.get_future(); // get a future
std::thread t(std::move(task)); // launch on a thread
// 從async()獲取的future
std::future<int> f2 = std::async(std::launch::async, []{ return 8; });
// 從promise獲取的future
std::promise<int> p;
std::future<int> f3 = p.get_future();
std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
// 等待所有future
std::cout << "Waiting..." << std::flush;
f1.wait();
f2.wait();
f3.wait();
std::cout << "Done!\nResults are: " << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';
t.join();
}
輸出:
Waiting...Done!
Results are: 7 8 9
2.1.4 多執行緒實現機制
多執行緒按並行內容可分為資料並行和任務並行兩種。其中資料並行是不同的執行緒攜帶不同的資料執行相同的邏輯,最經典的資料並行的應用是MMX指令、SIMD技術、Compute著色器等。任務並行是不同的執行緒執行不同的邏輯,資料可以相同,也可以不同,例如,遊戲引擎經常將檔案載入、音訊處理、網路接收乃至物理模擬都放到單獨的執行緒,以便它們可以並行地執行不同的任務。
多執行緒如果按劃分粒度和方式,則有線性劃分、遞迴劃分、任務型別劃分等。
線性劃分法的最簡單應用就是將連續陣列的元素平均分成若干份,每份資料派發到一個執行緒中執行,例如並行化的std::for_each
和UE裡的ParallelFor
。
線性劃分示意圖。連續資料被均分為若干份,接著派發到若干執行緒中並行地執行。
線上性劃分並行執行結束後,通常需要由呼叫執行緒合併和同步並行的結果。
遞迴劃分法是將連續資料按照某種規則劃分成若干份,每一份又可繼續劃分成更細粒度,直到某種規則停止劃分。常用於快速排序。
快速排序有兩個最基本的步驟:將資料劃分到中樞(pivot)元素之前或之後,然後對中樞元素之前和之後的兩半陣列再次進行快速排序。由於只有在一次排序結束後才能知道哪些項在中樞元素之前和之後,所以不能通過對資料的簡單(線性)劃分達到並行。當要對這種演算法進行並行化,很自然的會想到使用遞迴。每一級的遞迴都會多次呼叫quick_sort函式,因為需要知道哪些元素在中樞元素之前和之後。
遞迴劃分法示意圖。
將一個大框架內的邏輯劃分成若干個子任務,它們之間通常保持獨立,也可以有一定依賴,每個任務派發到一個執行緒執行,這就意味著真正意義上的執行緒獨立,每個執行緒只需要關注自己所要做的事情即可。
任務劃分示意圖。
合理地安排和劃分子任務,減少它們之間的依賴和等待同步,是提升執行效率的有利武器。不過,要做到這點,往往需要經過精細的設計實現以及反覆除錯和修改。
上面這種實現機制常被稱為Fork-Join(分叉-合併)並行模型,它和序列模型的執行機制對比如下圖:
上:序列執行模型;下:Fork-Join並行執行模型。
GDC2010的演講Task-based Multithreading - How to Program for 100 cores詳細闡述瞭如何採用基於Task的多執行緒執行機制:
基於Task的多執行緒比基於執行緒的架構要好很多,可以更加充分地利用多核優勢,使得每個核心都保持忙碌狀態:
該文還提到了如何將基於任務的多執行緒應用於排序、迷宮尋路等實際案例中。
2.2 現代圖形API的多執行緒特性
2.2.1 傳統圖形API的多執行緒特性
OpenGL及DirectX10之前版本的圖形API,所有的繪製指令是線性和阻塞式的,意味著每次呼叫Draw介面都不會立即返回,會卡住呼叫執行緒。這種CPU和GPU的互動機制在單核時代,對效能的影響不那麼突出,但是隨著多核時代的到來,這種互動機制顯然會嚴重影響執行效能。
若遊戲引擎的渲染器仍然是單執行緒的,這常常導致CPU的效能瓶頸,阻礙了利用多核計算資源來提高效能或豐富視覺化內容。
傳統圖形API線性執行繪製指令示意圖。
單執行緒渲染器通常會導致單個 CPU 核心滿負荷執行,而其他核心保持相對空閒,且效能低於可玩的幀率。
傳統圖形API在單執行緒單Context下設定渲染狀態呼叫繪製指令,並且繪製指令是阻塞式的,CPU和GPU無法並行執行,其它CPU核心也會處於空閒等待狀態。
在這些傳統圖形API架構多執行緒渲染,必須從軟體層面著手,開闢多個執行緒,用於單獨處理邏輯和渲染指令,以便解除CPU和GPU的相互等待耦合。早在SIGGraph2008有個Talk(Practical Parallel Rendering with DirectX 9 and 10)專門講解如何在DirectX9和10實現軟體級的多執行緒渲染,核心部分就是在軟體層錄製(Playback)D3D的繪製命令(Command)。
Practical Parallel Rendering with DirectX 9 and 10中提出的一種軟體級的多執行緒渲染架構。
不過,這種軟體層面的命令錄製存在多種問題,不支援部分圖形API(如狀態查詢),需額外的命令快取區記錄繪製指令,命令階段無法建立真正的GPU資源等等。
DirectX11嘗試從硬體層面解決多執行緒渲染的問題。它支援了兩種裝置上下文:即時上下文(Immediate Context)和延遲上下文(Deferred Context)。不同的延遲上下文可以同時在不同的執行緒中使用,生成將在“即時上下文”中執行的命令列表。這種多執行緒策略允許將複雜的場景分解成併發任務。
DirectX11的多執行緒模型。
不同的延遲上下文可以同時在不同的執行緒中使用,生成將在即時上下文中執行的命令列表。這種多執行緒策略允許將複雜的場景分解成併發任務。此外,延遲上下文在某些驅動的支援下,可實現硬體級的加速,而不必在即時上下文執行Command List。
為什麼使用Deferred Context的Command List提前錄製繪製指令會比直接使用Immediate Context呼叫繪製指令的效率更高?
答案在於Command List內部會對已經錄製的指令做高度的優化,執行這些優化後的指令會明顯提升效率,比直接單獨每條呼叫圖形API會高效得多。
在D3D11中命令列表中的命令是被快速記錄下來,而不是立即執行的,直到程式呼叫ExecuteCommandList方法(呼叫即返回,不等待)才被GPU真正的執行,此時那些使用延遲渲染裝置介面的CPU執行緒以及主渲染執行緒又可以去幹別的事情了,比如繼續下一幀的碰撞檢測、物理變換、動畫插值、光照準備等等,從而為記錄形成新的命令列表做準備。
不過,基於DirectX11的多執行緒架構,由於硬體層面的加速不是必然支援,所有在Deferred Context記錄的指令連同Immediate Context的指令必須由同一個執行緒(通常是渲染執行緒)提交給GPU執行。
DirectX11下的多執行緒架構示意圖。
這種非硬體支援的多執行緒渲染只是節省了部分CPU時間(多執行緒錄製指令和繪製同步等待),並不能從硬體層面真正發揮多執行緒的威力。
2.2.2 DirectX12的多執行緒特性
相較於DirectX11過渡性的偽多執行緒模型(稱之偽,是因為當時的大多數驅動並不支援DirectX11的硬體級多執行緒),DirectX 12 多執行緒則通過顯著減少 API 呼叫額外開銷得到了很大的改進,它取消了 DirectX 11 的裝置上下文的概念,直接使用Command List來呼叫 D3D APIs,然後通過命令佇列將命令列表提交給 GPU,並且所有 DirectX 12顯示卡都支援 DirectX 12 多執行緒的硬體加速。
DirectX12的多執行緒模型。
從原理上來看,DirectX12與DirectX11多執行緒渲染框架是類似的,都是通過在不同的CPU執行緒中錄製命令列表(Command Lists),最後再統一執行的方式完成多執行緒渲染。它們都從根本上遮蔽了令人髮指的Draw Call同步呼叫,而改為CPU和GPU完全非同步(並行)執行的方式,從而在整體渲染效率和效能上獲得巨大的提升。
對於DirectX12,使用者層面有3種命令佇列(Command Queue):複製佇列(Copy Queue)、計算佇列(Compute Queue)和3D佇列(3D Queue),它們可以並行地執行,並且通過柵欄(Fence)、訊號(Signal)或屏障(Barrier)來等待和同步。
GPU硬體層面則有3種引擎:複製引擎(Copy Engine)、計算引擎(Compute Engine)和3D引擎(3D Engine),它們也可以並行地執行,並且通過柵欄(Fence)、訊號(Signal)或屏障(Barrier)來等待和同步。
命令佇列可驅動GPU硬體的若干引擎,但有一定的限制,更具體地,3D Queue可以驅動GPU硬體的3種引擎,Compute Queue只能驅動Compute Engine和Copy Engine,Copy Queue僅可以驅動Copy Engine。
在CPU層面,可以有若干個執行緒,每個執行緒可建立產生若干個命令列表(Command List),每個命令列表可進入3種Command Queue的其中一種。當這些命令被GPU執行時,每種指令列表裡的命令會壓入不同的GPU引擎,以便它們並行地執行。(下圖)
DirectX12中的CPU執行緒、命令列表、命令佇列、GPU引擎之間的執行機制示意圖。
2.2.3 Vulkan的多執行緒特性
作為跨平臺圖形API的新生代表Vulkan,摒棄了傳統圖形API的弊端,直面多核時代的優勢,從而從設計和架構上發揮了並行渲染的威力。
綜合上看,Vulkan和DirectX12是非常接近的,都有著Command Buffer、CommandPool、Command Queue和Fence等核心概念,並行模式也非常相似:在不同的CPU執行緒並行地生成Command Buffer指令,最後由主執行緒收集這些Command Buffer並提交至GPU:
Vulkan圖形API並行示意圖。
並且,Vulkan的CommandPool可以每幀被不同的執行緒建立,以便減少同步等待,提升並行效率:
Vulkan中的CommandPool在不同幀之間的並行示意圖。
此外,Vulkan也存在著和DirectX12類似的各種同步機制:
Vulkan同步機制:semaphore(訊號)用於同步Queue;Fence(柵欄)用於同步GPU和CPU;Event(事件)和Barrier(屏障)用於同步Command Buffer。
關於Vulkan的更多用法、剖析、對比可參見文獻Evaluation of multi-threading in Vulkan。
2.2.4 Metal的多執行緒特性
Metal作為iOS和MacOS系統的專屬圖形API,也是新生代的代表,它既相容OpenGL這種傳統的圖形API用法,也支援類似Vulkan、DirectX12的新一代圖形API理念和架構。從使用者層面來看,Metal是比較友善的,提供了結構更清晰、概念更友好的API。
從OpenGL遷移到新生代圖形API的成本和收益對比。橫座標是從OpenGL(或ES)遷移其它圖形API的成本,縱座標是潛在的效能收益。可見Metal的遷移成本較低,但潛在的效能比也沒有Vulkan和DirectX12高。
Metal如同Vulkan和DirectX,有著很多相似的概念,諸如Command、Command Buffer、Command Queue及各類同步機制。
Metal基礎概念關係一覽表。其中Command Encoder有3種型別:MTLRenderCommandEncoder、MTLComputeCommandEncoder和MTLBlitCommandEncoder。CommandEncoder錄製命令之後,塞入Command Buffer,最終進入Command Queue命令佇列。
有了類似的概念和機制,Metal同樣可以方便地實現多執行緒錄製命令,且從硬體層面支援多執行緒排程:
Metal多執行緒模型示意圖。圖中顯示了3個CPU執行緒同時錄製不同型別的Encoder,每個執行緒都有專屬的Command Buffer,最終這些Command Buffer統一匯入Command Queue交由GPU執行。
2.3 遊戲引擎的多執行緒渲染
在正式講解UE的多執行緒渲染之前,先了解一下其它主流商業引擎的多執行緒架構和設計。
2.3.1 Unity
Unity的渲染體系中有幾個核心概念,一個是Client,執行於主執行緒(邏輯執行緒),負責產生渲染指令;另一個是Worker Thread,工作執行緒,用於協助處理主執行緒或生成渲染指令等各類子工作。Unity的渲染架構中支援以下幾種模式:
- Singlethreaded Rendering
單執行緒渲染模式,此模式下只有單個Client元件,沒有工作執行緒。唯一的Client在主執行緒中產生所有的渲染命令(rendering command,RCMD),並且擁有圖形裝置物件,也會在主執行緒向圖形裝置產生呼叫圖形API命令(graphics API,GCMD),它的排程示意圖如下:
這種模式下,CPU和GPU可能會相互等待,無法充分利用多核CPU,效能比較最差。
- **Multithreaded Rendering **
多執行緒渲染模式,這種模式下和單執行緒對比,就是多了一條工作執行緒,即用於生成GCMD的渲染執行緒,其中渲染執行緒跑的是GfxDeviceClient物件,專用於生成對應平臺的圖形API指令:
- Jobified Rendering
作業化渲染模式,此模式下有多個Client物件,單個渲染執行緒。此外,有多個作業物件,每個作業物件跑在專用獨立的執行緒,用於生成即時圖形命令(intermediate graphics commands,IGCMD)。此外,還有一個工作執行緒(渲染執行緒)用於將作業執行緒生成的IGCMD轉換成圖形API的GCMD,執行示意圖如下:
- Graphics Jobs
圖形化作業渲染模式,此模式下有多個Client,多個工作執行緒,沒有渲染執行緒。主執行緒上的多個Client物件驅動工作執行緒上的對應圖形裝置物件,直接生成GCMD,從而避免生成Jobified Rendering模式的IGCMD中間指令。只在支援硬體級多執行緒的圖形API上可啟用,如DirectX12、Vulkan等。執行示意圖如下:
**2.3.2 Frostbite **
Frostbite(寒霜)引擎在早期的時候,將每一幀分成個步驟:裁剪、構建、渲染,每個步驟所需的資料都放到雙緩衝內(double buffer),採用級聯方式執行,應用簡單的同步流程。它的執行示意圖如下:
而經過多年的進化,Frostbite在前幾年採用了幀圖(Frame Graph)的多執行緒渲染模式。該模式旨在將引擎的各類渲染功能(Feature)和上層渲染邏輯(Renderer)和下層資源(Shader、RenderContext、圖形API等)隔離開來,以便做進一步的解耦、優化,其中最重要的優化即開啟多執行緒渲染。
FrameGraph是高層級的Render Pass和資源的代表,包含了一幀中所用到的所有資訊。Pass之間可以指定順序和依賴關係,下圖是其中的一個示例:
寒霜引擎採用幀圖方式實現的延遲渲染的順序和依賴圖。
其中幀圖的每一幀資訊都有三個階段:建立(Setup)、編譯(Compile)和執行(Execute)。
建立階段就是建立各個Render Pass、輸入紋理、輸出紋理、依賴資源等等資訊。
編譯階段的工作主要是剔除未使用的Render Pass和資源,計算資源生命週期,以及根據使用標記建立對應的GPU資源,建立GPU資源時又做了大量的優化,諸如:簡化視訊記憶體分配演算法,在第一次使用時申請最後一次使用後釋放,非同步計算外部資源的生命週期,源於繫結標記的精確資源管理,扁平化所有資源的引用以提升GPU快取記憶體的命中率等等。編譯階段採用線性遍歷所有的RenderPass,遍歷時會計算資源引用次數、資源的最初和最後使用者、非同步等待點和資源屏障等等。
執行階段就按照Setup的順序執行(編譯階段也不會重新排序),只遍歷那些未被剔除的Render Pass並執行它們的回撥函式。如果是立即模式,則直接呼叫裝置上下文的API。執行階段才會根據編譯階段生成的handle真正獲取GPU資源。
最關鍵的是整個過程通過依賴圖(Dependency Grahp)實現自動化非同步計算。非同步機制在主時間軸開始,會自動同步在不同Queue裡的資源,同時會擴充套件它們的生命週期,以防意外釋放。當然,這個自動化系統也有副作用,如額外增加一定量的記憶體,可能會引發不可預期的效能瓶頸。所以,寒霜引擎支援手動模式,以便按照預期控制和更改非同步執行方式,從而逐Render Pass選擇性加入。
下圖可以比較簡潔明瞭說明非同步計算的執行機制:
寒霜引擎非同步計算示意圖。其中SSAO、SSAO Filter的Pass放入到非同步佇列,它們會寫入和讀取Raw AO的紋理,即便在同步點之前結束,但Raw AO的生命週期依然會被延長到同步點。
總之,幀圖的渲染架構得益於記錄了該幀所有的資訊,以至於可以通過資源別名(Resource Aliasing)節省大量的記憶體和視訊記憶體,可以實現半自動化的非同步計算,可以簡化渲染管線控制,可以製作出更加良好的視覺化和診斷工具。
2.3.3 Naughty Dog Engine
頑皮狗的遊戲引擎採用的也是作業系統,允許非GPU端的邏輯程式碼加入到作業系統。作業直接可以開啟和等待其它作業,對呼叫者隱藏記憶體管理細節,提供了簡潔易用的API,效能優化放在了第二位。
其中作業系統執行於纖程(Fiber),每個纖程類似區域性的執行緒,使用者層提供棧空間,其上下文包含少量的纖程狀態,以便減少暫存器的佔用。實際地執行線上程上,協作型的多執行緒模型。由於纖程非系統級的執行緒,切換上下文會非常快,只儲存和恢復暫存器的狀態(程式計數,棧指標,gpr等),故而開銷會很小。
作業系統會開闢若干條工作執行緒,每條工作執行緒會鎖定到GPU硬體核心。執行緒是執行單元,纖程是上下文,作業總是線上程的上下文內執行,採用原子計數器來同步。下圖是頑皮狗引擎的作業系統架構圖:
頑皮狗引擎作業系統架構圖。擁有6個工作執行緒,160個纖程,3個作業佇列。
作業可以向作業佇列新增新的作業,同時等待中的作業會放到專門的等待列表,每個等待中的作業會有引用計數,直到引用計數為0,才會從等待佇列中出列,以便繼續執行。
在頑皮狗引擎內,除了IO執行緒之外的所有東西都是作業,包括遊戲物體更新、動作更新和混合、射線檢測、渲染命令生成等等。可見將作業系統發揮得淋漓盡致,最大程度提升了並行的比例和效率。
為了提升幀率,將遊戲邏輯和渲染邏輯相分離,並行地執行,不過處理的是不同幀的資料,通常遊戲資料領先渲染資料一幀,而渲染邏輯又領先GPU資料一幀。
通過這樣的機制,可以避免CPU執行緒之間以及CPU和GPU之間的同步和等待,提升了幀率和吞吐量。
此外,它的記憶體分配也做了精緻的管理,比如引入了帶標籤的記憶體堆(Tagged Heap),記憶體堆以2M為一塊(Block),每個Block帶有一個標籤(Game、Render、GPU之一),分配器分配和釋放記憶體時是在標籤堆裡執行,避免直接向作業系統獲取:
此外,分配器支援為每個工作執行緒分配一個專屬的塊(跟TLS類似),避免資料同步和等待的時間,避免資料競險。
2.3.4 Destiny’s Engine
命運(Destiny)是一款第一人稱的動作角色扮演MMORPG,它使用的引擎也被稱為命運引擎(Destiny’s Engine)。
命運引擎在多執行緒架構上,採用的技術有基於任務的並行處理,作業管理設計和同步處理,作業的執行也是在纖程上。作業系統執行作業的優先順序是FIFO(先進先出),作業圖是異源架構,作業之間存在依賴,但沒有柵欄。
它將每一幀分成幾個步驟:模擬遊戲物體、物體裁剪、生成渲染命令、執行GPU相關工作、顯示。線上程設計上,會建立6條系統執行緒,每條執行緒的內容依次是:模擬迴圈,其它作業,渲染迴圈,音訊迴圈,作業核心和除錯Log,非同步任務、IO等。
在處理幀之間的資料,也是分離開遊戲模擬和渲染邏輯,遊戲模擬總是領先渲染一幀。遊戲模擬完之後,會將所有資料和狀態拷貝一份(映象,Mirror),以供下一幀的渲染使用:
命運引擎為了最大化CPU和GPU的並行效率,採取了動態載入平衡(dynamic load balancing)和智慧作業合批(smart job batching),具體做法是將所有渲染和可見性剔除的工作加入到任務系統,保持低延遲。下圖是並行化計算檢視作業的圖例:
此外,還將模擬邏輯從渲染邏輯中抽離和解耦,採用完全的資料驅動的渲染管線,所有的排序、記憶體分配、遍歷等演算法都遵循了快取記憶體一致性(結構體小量化,資料對齊,使得單個結構體資料能一次性被載入進快取記憶體行)。
2.4 UE的多執行緒機制
本章節主要剖析一下UE的多執行緒基礎、設計及架構,以便後面更好地切入到多執行緒渲染。
2.4.1 UE的多執行緒基礎
- TAtomic
UE的原子操作並沒有使用C++的Atomic模板,而是自己實現了一套,叫TAtomic。它提供的功能有載入、儲存、賦值等操作,在底層實現上,會採用平臺相關的原子操作介面實現:
// Engine\Source\Runtime\Core\Public\Templates\Atomic.h
template <typename T>
FORCEINLINE T Load(const volatile T* Element)
{
// 採取平臺相關的介面載入原子值.
auto Result = FPlatformAtomics::AtomicRead((volatile TUnderlyingIntegerType_T<T>*)Element);
return *(const T*)&Result;
}
template <typename T>
FORCEINLINE void Store(const volatile T* Element, T Value)
{
// 採取平臺相關的介面儲存原子值.
FPlatformAtomics::InterlockedExchange((volatile TUnderlyingIntegerType_T<T>*)Element, *(const TUnderlyingIntegerType_T<T>*)&Value);
}
template <typename T>
FORCEINLINE T Exchange(volatile T* Element, T Value)
{
// 採取平臺相關的介面交換原子值.
auto Result = FPlatformAtomics::InterlockedExchange((volatile TUnderlyingIntegerType_T<T>*)Element, *(const TUnderlyingIntegerType_T<T>*)&Value);
return *(const T*)&Result;
}
在記憶體順序上,不像C++提供了四種模式,UE做了簡化,只提供了兩種模式:
enum class EMemoryOrder
{
Relaxed, // 順序鬆散, 不會引起重排序
SequentiallyConsistent // 順序一致
};
需要注意的是,TAtomic雖然是模板類,但只對基本型別生效,UE是通過父類TAtomicBaseType_T
來達到檢測的目的:
template <typename T>
class TAtomic final : public UE4Atomic_Private::TAtomicBaseType_T<T>
{
static_assert(TIsTrivial<T>::Value, "TAtomic is only usable with trivial types");
(......)
}
- TFuture
UE實現了類似C++的Future和Promise物件,是模板類,抽象了返回值型別。以下是TFuture的宣告:
// Engine\Source\Runtime\Core\Public\Async\Future.h
template<typename InternalResultType>
class TFutureBase
{
public:
bool IsReady() const;
bool IsValid() const;
void Wait() const
{
if (State.IsValid())
{
while (!WaitFor(FTimespan::MaxValue()));
}
}
bool WaitFor(const FTimespan& Duration) const
{
return State.IsValid() ? State->WaitFor(Duration) : false;
}
bool WaitUntil(const FDateTime& Time) const
{
return WaitFor(Time - FDateTime::UtcNow());
}
protected:
typedef TSharedPtr<TFutureState<InternalResultType>, ESPMode::ThreadSafe> StateType;
const StateType& GetState() const;
template<typename Func>
auto Then(Func Continuation);
template<typename Func>
auto Next(Func Continuation);
void Reset();
private:
/** Holds the future's state. */
StateType State;
};
template<typename ResultType>
class TFuture : public TFutureBase<ResultType>
{
typedef TFutureBase<ResultType> BaseType;
public:
ResultType Get() const
{
return this->GetState()->GetResult();
}
TSharedFuture<ResultType> Share()
{
return TSharedFuture<ResultType>(MoveTemp(*this));
}
};
- TPromise
TPromise通常要和TFuture配合使用,如下所示:
template<typename InternalResultType>
class TPromiseBase : FNoncopyable
{
typedef TSharedPtr<TFutureState<InternalResultType>, ESPMode::ThreadSafe> StateType;
(......)
protected:
const StateType& GetState();
private:
StateType State; // 儲存了Future的狀態.
};
template<typename ResultType>
class TPromise : public TPromiseBase<ResultType>
{
public:
typedef TPromiseBase<ResultType> BaseType;
public:
// 獲取Future物件
TFuture<ResultType> GetFuture()
{
check(!FutureRetrieved);
FutureRetrieved = true;
return TFuture<ResultType>(this->GetState());
}
// 設定Future的值
FORCEINLINE void SetValue(const ResultType& Result)
{
EmplaceValue(Result);
}
FORCEINLINE void SetValue(ResultType&& Result)
{
EmplaceValue(MoveTemp(Result));
}
template<typename... ArgTypes>
void EmplaceValue(ArgTypes&&... Args)
{
this->GetState()->EmplaceResult(Forward<ArgTypes>(Args)...);
}
private:
bool FutureRetrieved;
};
- ParallelFor
ParallelFor是UE內建的支援多執行緒並行處理任務的For迴圈,在渲染系統中應用得相當普遍。它支援以下幾種並行方式:
enum class EParallelForFlags
{
None, // 預設並行方式
ForceSingleThread = 1, // 強制單執行緒, 常用於除錯.
Unbalanced = 2, // 非任務平衡, 常用於具有高度可變計算時間的任務.
PumpRenderingThread = 4, // 注入渲染執行緒. 如果是在渲染執行緒呼叫, 需要保證ProcessThread空閒狀態.
};
支援的ParallelFor呼叫方式如下:
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, bool bForceSingleThread, bool bPumpRenderingThread=false);
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
template<typename FunctionType>
inline void ParallelForTemplate(int32 Num, const FunctionType& Body, EParallelForFlags Flags = EParallelForFlags::None);
inline void ParallelForWithPreWork(int32 Num, TFunctionRef<void(int32)> Body, TFunctionRef<void()> CurrentThreadWorkToDoBeforeHelping, bool bForceSingleThread, bool bPumpRenderingThread = false);
inline void ParallelForWithPreWork(int32 Num, TFunctionRef<void(int32)> Body, TFunctionRef<void()> CurrentThreadWorkToDoBeforeHelping, EParallelForFlags Flags = EParallelForFlags::None);
ParallelFor是基於TaskGraph機制實現的,由於TaskGraph後面才提到,這裡就不涉及其實現。下面展示UE的一個應用案例:
// Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp
// 並行化增加Primitive到場景的用例.
void FRegisterComponentContext::Process()
{
FSceneInterface* Scene = World->Scene;
ParallelFor(AddPrimitiveBatches.Num(), // 數量
[&](int32 Index) //回撥函式, Index返回索引
{
if (!AddPrimitiveBatches[Index]->IsPendingKill())
{
Scene->AddPrimitive(AddPrimitiveBatches[Index]);
}
},
!FApp::ShouldUseThreadingForPerformance() // 是否多執行緒處理
);
AddPrimitiveBatches.Empty();
}
- 基礎模板
UnrealTemplate.h定義了很多基礎模板,用於資料轉換、拷貝、轉移等功能。下面例舉部分常見的函式和型別:
模板名 | 解析 | stl對映 |
---|---|---|
template ReferencedType* IfAThenAElseB(ReferencedType* A,ReferencedType* B) |
返回A ? A : B | - |
template void Move(T& A,typename TMoveSupportTraits |
釋放A,將B的資料替換到A,但不會影響B的資料。 | - |
template void Move(T& A,typename TMoveSupportTraits |
釋放A,將B的資料替換到A,但會影響B的資料。 | - |
FNoncopyable | 派生它即可實現不可拷貝的物件。 | - |
TGuardValue | 帶作業域的值,可指定一個新值和舊值,作用域內是新值,離開作用域變成舊值。 | - |
TScopeCounter | 帶作用域的計數器,作用域內計數器+1,離開作用域後計數器-1 | - |
template typename TRemoveReference |
將引用轉換成右值,可能會修改源值。 | std::move |
template T CopyTemp(T& Val) |
強制建立右值的拷貝,不會改變源值。 | - |
template T&& Forward(typename TRemoveReference |
將引用轉換成右值引用。 | std::forward |
template <typename T, typename ArgType> T StaticCast(ArgType&& Arg) |
靜態型別轉換。 | static_cast |
2.4.2 UE的多執行緒實現
UE的多執行緒實現上並沒有採納C++11標準庫的那一套,而是自己從系統級做了封裝和實現,包括系統執行緒、執行緒池、非同步任務、任務圖以及相關的通知和同步機制。
2.4.2.1 FRunnable
FRunnable
是所有可以在多個執行緒並行地執行的物體的父類,它提供的基礎介面如下:
// Engine\Source\Runtime\Core\Public\HAL\Runnable.h
class CORE_API FRunnable
{
public:
virtual bool Init(); // 初始化, 成功返回True.
virtual uint32 Run(); // 執行, 只有Init成功才會被呼叫.
virtual void Stop(); // 請求提前停止.
virtual void Exit(); // 退出, 清理資料.
};
FRunnable
及其子類是可執行於多執行緒的物件,而與之對立的是隻在單執行緒執行的類FSingleThreadRunnable
:
// Engine\Source\Runtime\Core\Public\Misc\SingleThreadRunnable.h
// 多執行緒禁用下的單執行緒執行的物體
class CORE_API FSingleThreadRunnable
{
public:
virtual void Tick();
};
FRunnable
的子類非常多,以下是常見的部分核心子類及其解析。
-
FRenderingThread:執行於渲染執行緒上的物件。後面有章節會專門剖析。
-
FRHIThread:執行於RHI執行緒上的物件。後面有章節會專門剖析。
-
FRenderingThreadTickHeartbeat:執行於心跳渲染執行緒上的物體。
-
FTaskThreadBase:線上程執行的任務父類,後面會有章節專門解析這部分。
-
FQueuedThread:可儲存線上程池的執行緒父類。提供的介面如下:
// Engine\Source\Runtime\Core\Private\HAL\ThreadingBase.cpp class FQueuedThread : public FRunnable { protected: FEvent* DoWorkEvent; // 任務執行完畢的事件. TAtomic<bool> TimeToDie; // 是否需要超時. IQueuedWork* volatile QueuedWork; // 被執行的任務. class FQueuedThreadPoolBase* OwningThreadPool; // 所在的執行緒池. FRunnableThread* Thread; // 真正用於執行任務的執行緒. virtual uint32 Run() override; public: virtual bool Create(class FQueuedThreadPoolBase* InPool,uint32 InStackSize,EThreadPriority ThreadPriority); bool KillThread(); void DoWork(IQueuedWork* InQueuedWork); };
-
TAsyncRunnable:非同步地在單獨執行緒執行的任務,是個模板類,宣告如下:
// Engine\Source\Runtime\Core\Public\Async\Async.h template<typename ResultType> class TAsyncRunnable: public FRunnable { public: virtual uint32 Run() override; private: TUniqueFunction<ResultType()> Function; TPromise<ResultType> Promise; TFuture<FRunnableThread*> ThreadFuture; };
-
FAsyncPurge:輔助類,提供銷燬位於工作執行緒的UObject物件。
由此可見,FRunnable物件並不能獨立存在,總是要依賴執行緒來真正地執行任務。
另外,還需要特意提出:FRenderingThread、FQueuedThread聽名字像是真正的執行緒,然而並不是,只是用於處理某些特定任務的可執行物體,實際上還是要依賴它們內部FRunnableThread的成員物件來執行。
2.4.2.2 FRunnableThread
FRunnableThread是可執行執行緒的父類,提供了一組用於管理執行緒生命週期的介面。它提供的基礎介面和解析如下:
// Engine\Source\Runtime\Core\Public\HAL\RunnableThread.h
class CORE_API FRunnableThread
{
static uint32 RunnableTlsSlot; // FRunnableThread的TLS插槽索引.
public:
static uint32 GetTlsSlot();
// 靜態類, 用於建立執行緒, 需提供一個FRunnable物件, 用於執行緒執行的任務.
static FRunnableThread* Create(FRunnable* InRunnable, const TCHAR* ThreadName, uint32 InStackSize = 0,
EThreadPriority InThreadPri, uint64 InThreadAffinityMask,EThreadCreateFlags InCreateFlags);
// 設定執行緒優先順序.
virtual void SetThreadPriority( EThreadPriority NewPriority );
// 暫停/繼續執行執行緒
virtual void Suspend( bool bShouldPause = true );
// 銷燬執行緒, 通常需要指定等待標記bShouldWait為true, 否則可能引起記憶體洩漏或死鎖!
virtual bool Kill( bool bShouldWait = true );
// 等待執行完畢, 會卡呼叫執行緒.
virtual void WaitForCompletion();
const uint32 GetThreadID() const;
const FString& GetThreadName() const;
protected:
FString ThreadName;
FRunnable* Runnable; // 被執行物件
FEvent* ThreadInitSyncEvent; // 執行緒初始化完成同步事件, 防止執行緒未初始化完畢就執行任務.
uint64 ThreadAffinityMask; // 親和標記, 用於執行緒傾向指定的CPU核心執行.
TArray<FTlsAutoCleanup*> TlsInstances; // 執行緒消耗時需要一起清理的Tls物件.
EThreadPriority ThreadPriority;
uint32 ThreadID;
private:
virtual void Tick();
};
需要注意的是,FRunnableThread提供了靜態建立介面,建立執行緒時需要指定一個FRunnable物件,作為執行緒執行的任務。它是一個基礎父類,下面是繼承自它的部分核心子類及解析:
-
FRunnableThreadWin:Windows平臺的執行緒實現。它的介面和實現如下:
// Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.h class FRunnableThreadWin : public FRunnableThread { HANDLE Thread; // 執行緒控制程式碼 // 執行緒回撥介面, 建立執行緒時作為引數傳入. static ::DWORD STDCALL _ThreadProc( LPVOID pThis ) { check(pThis); return ((FRunnableThreadWin*)pThis)->GuardedRun(); } uint32 GuardedRun(); uint32 Run(); public: // 轉換優先順序 static int TranslateThreadPriority(EThreadPriority Priority) { switch (Priority) { case TPri_AboveNormal: return THREAD_PRIORITY_HIGHEST; case TPri_Normal: return THREAD_PRIORITY_HIGHEST - 1; case TPri_BelowNormal: return THREAD_PRIORITY_HIGHEST - 3; case TPri_Highest: return THREAD_PRIORITY_HIGHEST; case TPri_TimeCritical: return THREAD_PRIORITY_HIGHEST; case TPri_Lowest: return THREAD_PRIORITY_HIGHEST - 4; case TPri_SlightlyBelowNormal: return THREAD_PRIORITY_HIGHEST - 2; default: UE_LOG(LogHAL, Fatal, TEXT("Unknown Priority passed to TranslateThreadPriority()")); return TPri_Normal; } } // 設定優先順序 virtual void SetThreadPriority( EThreadPriority NewPriority ) override { // Don't bother calling the OS if there is no need ThreadPriority = NewPriority; // Change the priority on the thread ::SetThreadPriority(Thread, TranslateThreadPriority(ThreadPriority)); } virtual void Suspend( bool bShouldPause = true ) override { check(Thread); if (bShouldPause == true) { SuspendThread(Thread); } else { ResumeThread(Thread); } } virtual bool Kill( bool bShouldWait = false ) override { check(Thread && "Did you forget to call Create()?"); bool bDidExitOK = true; // 先停止Runnable物件, 使得其有清理資料的機會 if (Runnable) { Runnable->Stop(); } // 等待執行緒處理完畢. if (bShouldWait == true) { // Wait indefinitely for the thread to finish. IMPORTANT: It's not safe to just go and // kill the thread with TerminateThread() as it could have a mutex lock that's shared // with a thread that's continuing to run, which would cause that other thread to // dead-lock. (This can manifest itself in code as simple as the synchronization // object that is used by our logging output classes. Trust us, we've seen it!) WaitForSingleObject(Thread,INFINITE); } // 關閉執行緒控制程式碼 CloseHandle(Thread); Thread = NULL; return bDidExitOK; } virtual void WaitForCompletion( ) override { // Block until this thread exits WaitForSingleObject(Thread,INFINITE); } protected: virtual bool CreateInternal( FRunnable* InRunnable, const TCHAR* InThreadName, uint32 InStackSize = 0, EThreadPriority InThreadPri = TPri_Normal, uint64 InThreadAffinityMask = 0, EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None) override { static bool bOnce = false; if (!bOnce) { bOnce = true; ::SetThreadPriority(::GetCurrentThread(), TranslateThreadPriority(TPri_Normal)); // set the main thread to be normal, since this is no longer the windows default. } check(InRunnable); Runnable = InRunnable; ThreadAffinityMask = InThreadAffinityMask; // 建立初始化完成同步事件. ThreadInitSyncEvent = FPlatformProcess::GetSynchEventFromPool(true); ThreadName = InThreadName ? InThreadName : TEXT("Unnamed UE4"); // Create the new thread { LLM_SCOPE(ELLMTag::ThreadStack); LLM_PLATFORM_SCOPE(ELLMTag::ThreadStackPlatform); // add in the thread size, since it's allocated in a black box we can't track LLM(FLowLevelMemTracker::Get().OnLowLevelAlloc(ELLMTracker::Default, nullptr, InStackSize)); LLM(FLowLevelMemTracker::Get().OnLowLevelAlloc(ELLMTracker::Platform, nullptr, InStackSize)); // 呼叫Windows API建立執行緒. Thread = CreateThread(NULL, InStackSize, _ThreadProc, this, STACK_SIZE_PARAM_IS_A_RESERVATION | CREATE_SUSPENDED, (::DWORD *)&ThreadID); } // If it fails, clear all the vars if (Thread == NULL) { Runnable = nullptr; } else { // 加入到執行緒管理器中. FThreadManager::Get().AddThread(ThreadID, this); ResumeThread(Thread); // Let the thread start up ThreadInitSyncEvent->Wait(INFINITE); SetThreadPriority(InThreadPri); } // 清理同步事件 FPlatformProcess::ReturnSynchEventToPool(ThreadInitSyncEvent); ThreadInitSyncEvent = nullptr; return Thread != NULL; } };
從上面程式碼可看出,Windows平臺的執行緒直接呼叫Windows API建立和同步資訊,從而實現執行緒的平臺抽象,從平臺依賴抽離出來。
-
FRunnableThreadPThread:POSIX Thread(簡稱PThread)的父類,常用於類Unix POSIX 系統,如Linux、Solaris、Apple等。其實現和Windows平臺類似,這裡就不展開其程式碼解析了。它的子類有:
-
FRunnableThreadApple:蘋果系統(MacOS、iOS)的執行緒。
-
FRunnableThreadAndroid:安卓系統的執行緒。
-
FRunnableThreadUnix:Unix系統的執行緒。
-
-
FRunnableThreadHoloLens:HoloLens系統的執行緒。
-
FFakeThread:假執行緒,多執行緒被禁用後的代替品,實際執行於單個執行緒。
FRunnable和FRunnableThread是相輔相成的,缺一而不可,一個是執行的載體,一個是執行的內容。下面是它們的一個應用示例:
// 派生FRunnable
class FMyRunnable : public FRunnable
{
bool bStop;
public:
virtual bool Init(void)
{
bStop = false;
return true;
}
virtual uint32 Run(void)
{
for (int32 i = 0; i < 10 && !bStop; i++)
{
FPlatformProcess::Sleep(1.0f);
}
return 0;
}
virtual void Stop(void)
{
bStop = true;
}
virtual void Exit(void)
{
}
};
void TestRunnableAndRunnableThread()
{
// 建立Runnable物件
FMyRunnable* MyRunnable = new FMyRunnable;
// 建立執行緒, 傳入MyRunnable
FRunnableThread* MyThread = FRunnableThread::Create(MyRunnable, TEXT("MyRunnable"));
// 暫停當前執行緒
FPlatformProcess::Sleep(4.0f);
// 等待執行緒結束
MyRunnable->Stop();
MyThread->WaitForCompletion();
// 清理資料.
delete MyThread;
delete MyRunnable;
}
細心的同學應該有注意到,建立執行緒的時候,會將執行緒加入到FThreadManager中,也就是說所有的執行緒都由FThreadManager來管理。以下是FThreadManager的宣告:
// Engine\Source\Runtime\Core\Public\HAL\ThreadManager.h
class FThreadManager
{
FCriticalSection ThreadsCritical; // 修改執行緒列表Threads的臨界區
static bool bIsInitialized;
TMap<uint32, class FRunnableThread*, TInlineSetAllocator<256>> Threads; // 執行緒列表, 注意資料結構是Map, Key是執行緒ID.
public:
void AddThread(uint32 ThreadId, class FRunnableThread* Thread); // 增加執行緒
void RemoveThread(class FRunnableThread* Thread); // 刪除執行緒
void Tick(); // 幀更新, 只對FFakeThread起作用.
const FString& GetThreadName(uint32 ThreadId);
void ForEachThread(TFunction<void(uint32, class FRunnableThread*)> Func); // 遍歷執行緒
static bool IsInitialized();
static FThreadManager& Get();
};
2.4.2.3 QueuedWork
本節將闡述UE的佇列化QueuedWork體系,包含IQueuedWork、TAsyncQueuedWork、FQueuedThreadPool、FQueuedThreadPoolBase等。
- IQueuedWork和TAsyncQueuedWork
IQueuedWork是一組抽象介面,儲存著一組佇列化的任務物件,會被FQueuedThreadPool執行緒池物件執行。IQueuedWork的介面如下:
// Engine\Source\Runtime\Core\Public\Misc\IQueuedWork.h
class IQueuedWork
{
public:
virtual void DoThreadedWork() = 0; // 執行佇列化的任務.
virtual void Abandon() = 0; // 提前放棄執行, 並通知佇列裡的所有物件清理資料.
};
由於IQueuedWork只是抽象類,並沒有實際執行程式碼,故而主要子類TAsyncQueuedWork承擔了實現程式碼的任務,以下是TAsyncQueuedWork的宣告和實現:
// Engine\Source\Runtime\Core\Public\Async\Async.h
template<typename ResultType>
class TAsyncQueuedWork : public IQueuedWork
{
public:
virtual void DoThreadedWork() override
{
SetPromise(Promise, Function);
delete this;
}
virtual void Abandon() override
{
// not supported
}
private:
TUniqueFunction<ResultType()> Function; // 被執行的函式列表.
TPromise<ResultType> Promise; // 用於同步的物件
};
- FQueuedThreadPool和FQueuedThreadPoolBase
與FRunnable和FRunnableThread類似,TAsyncQueuedWork也不能獨立地執行任務,需要依賴FQueuedThreadPool來執行。下面是FQueuedThreadPool的宣告:
// Engine\Source\Runtime\Core\Public\Misc\QueuedThreadPool.h
// 執行IQueuedWork任務列表的執行緒池.
class FQueuedThreadPool
{
public:
// 建立指定數量、棧大小和優先順序的執行緒。
virtual bool Create( uint32 InNumQueuedThreads, uint32 StackSize = (32 * 1024), EThreadPriority ThreadPriority=TPri_Normal ) = 0;
// 銷燬執行緒內的後臺執行緒.
virtual void Destroy() = 0;
// 加入佇列化任務. 如果有可用的執行緒, 則立即執行; 否則會稍後再執行.
virtual void AddQueuedWork( IQueuedWork* InQueuedWork ) = 0;
// 撤銷指定佇列化任務.
virtual bool RetractQueuedWork( IQueuedWork* InQueuedWork ) = 0;
// 獲取執行緒數量.
virtual int32 GetNumThreads() const = 0;
public:
// 建立執行緒池物件.
static FQueuedThreadPool* Allocate();
// 重寫棧大小.
static uint32 OverrideStackSize;
};
上面可以看出,FQueuedThreadPool是抽象類,只提供介面,並沒有實現。實際上,實現是在FQueuedThreadPoolBase中,如下:
// Engine\Source\Runtime\Core\Private\HAL\ThreadingBase.cpp
class FQueuedThreadPoolBase : public FQueuedThreadPool
{
protected:
TArray<IQueuedWork*> QueuedWork; // 需要執行的任務列表
TArray<FQueuedThread*> QueuedThreads; // 執行緒池內的可用執行緒
TArray<FQueuedThread*> AllThreads; // 執行緒池內的所有執行緒
FCriticalSection* SynchQueue; // 同步臨界區
bool TimeToDie; // 超時標記
public:
FQueuedThreadPoolBase()
: SynchQueue(nullptr)
, TimeToDie(0)
{ }
virtual ~FQueuedThreadPoolBase()
{
Destroy();
}
virtual bool Create(uint32 InNumQueuedThreads,uint32 StackSize = (32 * 1024),EThreadPriority ThreadPriority=TPri_Normal) override
{
// 處理同步鎖.
bool bWasSuccessful = true;
check(SynchQueue == nullptr);
SynchQueue = new FCriticalSection();
FScopeLock Lock(SynchQueue);
// Presize the array so there is no extra memory allocated
check(QueuedThreads.Num() == 0);
QueuedThreads.Empty(InNumQueuedThreads);
if( OverrideStackSize > StackSize )
{
StackSize = OverrideStackSize;
}
// 建立執行緒, 注意建立的是FQueuedThread.
for (uint32 Count = 0; Count < InNumQueuedThreads && bWasSuccessful == true; Count++)
{
FQueuedThread* pThread = new FQueuedThread();
// 利用FQueuedThread物件建立真正的執行緒.
if (pThread->Create(this,StackSize,ThreadPriority) == true)
{
QueuedThreads.Add(pThread);
AllThreads.Add(pThread);
}
else
{
// 建立失敗, 清理執行緒物件.
bWasSuccessful = false;
delete pThread;
}
}
// 建立執行緒池失敗, 清理資料.
if (bWasSuccessful == false)
{
Destroy();
}
return bWasSuccessful;
}
virtual void Destroy() override
{
if (SynchQueue)
{
{
FScopeLock Lock(SynchQueue);
TimeToDie = 1;
FPlatformMisc::MemoryBarrier();
// Clean up all queued objects
for (int32 Index = 0; Index < QueuedWork.Num(); Index++)
{
QueuedWork[Index]->Abandon();
}
// Empty out the invalid pointers
QueuedWork.Empty();
}
// 等待所有執行緒執行完成, 注意這裡並沒有使用同步時間, 而是使用類似自旋鎖的機制.
while (1)
{
{
// 訪問AllThreads和QueuedThreads的資料時先鎖定臨界區. 防止其它執行緒修改資料.
FScopeLock Lock(SynchQueue);
if (AllThreads.Num() == QueuedThreads.Num())
{
break;
}
}
FPlatformProcess::Sleep(0.0f); // 切換當前執行緒時間片, 防止當前執行緒佔用cpu時鐘.
}
// 刪除所有執行緒.
{
FScopeLock Lock(SynchQueue);
// Now tell each thread to die and delete those
for (int32 Index = 0; Index < AllThreads.Num(); Index++)
{
AllThreads[Index]->KillThread();
delete AllThreads[Index];
}
QueuedThreads.Empty();
AllThreads.Empty();
}
// 刪除同步鎖.
delete SynchQueue;
SynchQueue = nullptr;
}
}
int32 GetNumQueuedJobs() const
{
return QueuedWork.Num();
}
virtual int32 GetNumThreads() const
{
return AllThreads.Num();
}
// 加入佇列化任務.
void AddQueuedWork(IQueuedWork* InQueuedWork) override
{
check(InQueuedWork != nullptr);
if (TimeToDie)
{
InQueuedWork->Abandon();
return;
}
check(SynchQueue);
FQueuedThread* Thread = nullptr;
{
// 操作執行緒池裡的所有資料前都需要鎖定臨界區.
FScopeLock sl(SynchQueue);
const int32 AvailableThreadCount = QueuedThreads.Num();
// 沒有可用執行緒, 加入任務佇列, 稍後再執行.
if (AvailableThreadCount == 0)
{
QueuedWork.Add(InQueuedWork);
return;
}
// 從可用執行緒池中獲取一個執行緒, 並將其從可用執行緒池中刪除.
const int32 ThreadIndex = AvailableThreadCount - 1;
Thread = QueuedThreads[ThreadIndex];
QueuedThreads.RemoveAt(ThreadIndex, 1, /* do not allow shrinking */ false);
}
// 執行任務
Thread->DoWork(InQueuedWork);
}
virtual bool RetractQueuedWork(IQueuedWork* InQueuedWork) override
{
if (TimeToDie)
{
return false; // no special consideration for this, refuse the retraction and let shutdown proceed
}
check(InQueuedWork != nullptr);
check(SynchQueue);
FScopeLock sl(SynchQueue);
return !!QueuedWork.RemoveSingle(InQueuedWork);
}
// 如果有可用任務,則獲取一個並執行, 否則將執行緒迴歸可用執行緒池. 此介面由FQueuedThread呼叫.
IQueuedWork* ReturnToPoolOrGetNextJob(FQueuedThread* InQueuedThread)
{
check(InQueuedThread != nullptr);
IQueuedWork* Work = nullptr;
// Check to see if there is any work to be done
FScopeLock sl(SynchQueue);
if (TimeToDie)
{
check(!QueuedWork.Num()); // we better not have anything if we are dying
}
if (QueuedWork.Num() > 0)
{
// Grab the oldest work in the queue. This is slower than
// getting the most recent but prevents work from being
// queued and never done
Work = QueuedWork[0];
// Remove it from the list so no one else grabs it
QueuedWork.RemoveAt(0, 1, /* do not allow shrinking */ false);
}
if (!Work)
{
// There was no work to be done, so add the thread to the pool
QueuedThreads.Add(InQueuedThread);
}
return Work;
}
};
上面的介面ReturnToPoolOrGetNextJob並非FQueuedThreadPoolBase呼叫,而是由正在執行任務且執行完畢的FQueuedThread物件主動呼叫,如下所示:
uint32 FQueuedThread::Run()
{
while (!TimeToDie.Load(EMemoryOrder::Relaxed))
{
bool bContinueWaiting = true;
(......)
// 讓事件等待.
if (bContinueWaiting)
{
DoWorkEvent->Wait();
}
IQueuedWork* LocalQueuedWork = QueuedWork;
QueuedWork = nullptr;
FPlatformMisc::MemoryBarrier();
check(LocalQueuedWork || TimeToDie.Load(EMemoryOrder::Relaxed)); // well you woke me up, where is the job or termination request?
// 不斷地從執行緒池獲取任務並執行, 直到執行緒池的所有任務執行完畢.
while (LocalQueuedWork)
{
// 執行任務.
LocalQueuedWork->DoThreadedWork();
// 從執行緒池獲取下一個任務.
LocalQueuedWork = OwningThreadPool->ReturnToPoolOrGetNextJob(this);
}
}
return 0;
}
從上面可以看出,FQueuedThreadPool和FQueuedThread的資料和介面巧妙地配合,從而並行化地執行任務。
- GThreadPool
執行緒池的機制已經講述完畢,下面講一下UE的全域性執行緒池GThreadPool的初始化過程,此過程在FEngineLoop::PreInitPreStartupScreen中,1.4.6.1 引擎預初始化已經有提及:
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
{
TRACE_THREAD_GROUP_SCOPE("ThreadPool");
// 建立全域性執行緒池
GThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
// 如果是純伺服器模式, 執行緒池只有一個執行緒.
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 1;
}
// 建立工作執行緒相等的執行緒數量.
verify(GThreadPool->Create(NumThreadsInThreadPool, StackSize * 1024, TPri_SlightlyBelowNormal));
}
(......)
}
如果需要GThreadPool為我們做事,則使用示例如下:
// Engine\Source\Runtime\Engine\Private\ShadowMap.cpp
// 多執行緒編碼紋理
if (bMultithreadedEncode)
{
// 完成的任務計數器.
FThreadSafeCounter Counter(PendingTextures.Num());
// 待編碼的紋理任務列表
TArray<FAsyncEncode<FShadowMapPendingTexture>> AsyncEncodeTasks;
AsyncEncodeTasks.Empty(PendingTextures.Num());
// 建立所有任務, 加入到AsyncEncodeTasks列表中.
for (auto& PendingTexture : PendingTextures)
{
PendingTexture.CreateUObjects();
// 建立AsyncEncodeTask
auto AsyncEncodeTask = new (AsyncEncodeTasks)FAsyncEncode<FShadowMapPendingTexture>(&PendingTexture, LightingScenario, Counter, TextureCompressorModule);
// 將AsyncEncodeTask加入全域性執行緒池並執行.
GThreadPool->AddQueuedWork(AsyncEncodeTask);
}
// 如果還有任務未完成, 則讓當前執行緒進入睡眠狀態.
while (Counter.GetValue() > 0)
{
GWarn->UpdateProgress(Counter.GetValue(), PendingTextures.Num());
FPlatformProcess::Sleep(0.0001f);
}
}
2.4.2.4 TaskGraph
TaskGraph直譯是任務圖,使用的圖是DAG(Directed Acyclic Graph,有向非迴圈圖),可以指定依賴關係,指定前序和後序任務,但不能有迴圈依賴。它是UE內迄今為止最為複雜的並行任務系統,涉及的概念、執行機制的複雜度都陡增,本節將花大篇幅描述它們,旨在闡述清楚它們的機制和原理。
- FBaseGraphTask
FBaseGraphTask是執行於TaskGraph的任務,是個基礎父類,其派生的具體任務子類才會執行任務。它的宣告(節選)如下:
// Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h
class FBaseGraphTask
{
protected:
FBaseGraphTask(int32 InNumberOfPrerequistitesOutstanding);
// 先決任務完成或部分地完成.
void PrerequisitesComplete(ENamedThreads::Type CurrentThread, int32 NumAlreadyFinishedPrequistes, bool bUnlock = true);
// 帶條件(前置任務都已經執行完畢)地執行任務
void ConditionalQueueTask(ENamedThreads::Type CurrentThread)
{
if (NumberOfPrerequistitesOutstanding.Decrement()==0)
{
QueueTask(CurrentThread);
}
}
private:
// 真正地執行任務, 由子類實現.
virtual void ExecuteTask(TArray<FBaseGraphTask*>& NewTasks, ENamedThreads::Type CurrentThread)=0;
// 加入到TaskGraph任務佇列中.
void QueueTask(ENamedThreads::Type CurrentThreadIfKnown)
{
checkThreadGraph(LifeStage.Increment() == int32(LS_Queued));
FTaskGraphInterface::Get().QueueTask(this, ThreadToExecuteOn, CurrentThreadIfKnown);
}
ENamedThreads::Type ThreadToExecuteOn; // 執行任務的執行緒型別
FThreadSafeCounter NumberOfPrerequistitesOutstanding; // 執行任務前的計數器
};
- TGraphTask
FBaseGraphTask的唯一子類TGraphTask承接了完成執行任務的程式碼。TGraphTask的宣告和實現如下:
template<typename TTask>
class TGraphTask final : public FBaseGraphTask
{
public:
// 構造任務的輔助類.
class FConstructor
{
public:
// 建立TTask任務物件, 然後設定TGraphTask任務的資料, 以便在適當時機執行.
template<typename...T>
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
return Owner->Setup(Prerequisites, CurrentThreadIfKnown);
}
// 建立TTask任務物件, 然後設定TGraphTask任務的資料, 並持有但不執行.
template<typename...T>
TGraphTask* ConstructAndHold(T&&... Args)
{
new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
return Owner->Hold(Prerequisites, CurrentThreadIfKnown);
}
private:
TGraphTask* Owner; // 所在的TGraphTask物件.
const FGraphEventArray* Prerequisites; // 先決任務.
ENamedThreads::Type CurrentThreadIfKnown;
};
// 建立任務, 注意返回的是FConstructor物件, 以便對任務執行後續操作.
static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
int32 NumPrereq = Prerequisites ? Prerequisites->Num() : 0;
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
return FConstructor(new (Mem) TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
return FConstructor(new TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
void Unlock(ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
ConditionalQueueTask(CurrentThreadIfKnown);
}
FGraphEventRef GetCompletionEvent()
{
return Subsequents;
}
private:
// 執行任務
void ExecuteTask(TArray<FBaseGraphTask*>& NewTasks, ENamedThreads::Type CurrentThread) override
{
(......)
// 處理後續任務.
if (TTask::GetSubsequentsMode() == ESubsequentsMode::TrackSubsequents)
{
Subsequents->CheckDontCompleteUntilIsEmpty(); // we can only add wait for tasks while executing the task
}
// 執行任務
TTask& Task = *(TTask*)&TaskStorage;
{
FScopeCycleCounter Scope(Task.GetStatId(), true);
Task.DoTask(CurrentThread, Subsequents);
Task.~TTask();
checkThreadGraph(ENamedThreads::GetThreadIndex(CurrentThread) <= ENamedThreads::GetRenderThread() || FMemStack::Get().IsEmpty()); // you must mark and pop memstacks if you use them in tasks! Named threads are excepted.
}
TaskConstructed = false;
// 執行後序任務.
if (TTask::GetSubsequentsMode() == ESubsequentsMode::TrackSubsequents)
{
FPlatformMisc::MemoryBarrier();
Subsequents->DispatchSubsequents(NewTasks, CurrentThread);
}
// 釋放任務物件資料.
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
this->TGraphTask::~TGraphTask();
FBaseGraphTask::GetSmallTaskAllocator().Free(this);
}
else
{
delete this;
}
}
// 設定先決任務.
void SetupPrereqs(const FGraphEventArray* Prerequisites, ENamedThreads::Type CurrentThreadIfKnown, bool bUnlock)
{
checkThreadGraph(!TaskConstructed);
TaskConstructed = true;
TTask& Task = *(TTask*)&TaskStorage;
SetThreadToExecuteOn(Task.GetDesiredThread());
int32 AlreadyCompletedPrerequisites = 0;
if (Prerequisites)
{
for (int32 Index = 0; Index < Prerequisites->Num(); Index++)
{
check((*Prerequisites)[Index]);
if (!(*Prerequisites)[Index]->AddSubsequent(this))
{
AlreadyCompletedPrerequisites++;
}
}
}
PrerequisitesComplete(CurrentThreadIfKnown, AlreadyCompletedPrerequisites, bUnlock);
}
// 設定任務資料.
FGraphEventRef Setup(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
FGraphEventRef ReturnedEventRef = Subsequents; // very important so that this doesn't get destroyed before we return
SetupPrereqs(Prerequisites, CurrentThreadIfKnown, true);
return ReturnedEventRef;
}
// 持有任務資料.
TGraphTask* Hold(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
SetupPrereqs(Prerequisites, CurrentThreadIfKnown, false);
return this;
}
// 建立任務.
static FConstructor CreateTask(FGraphEventRef SubsequentsToAssume, const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
return FConstructor(new (Mem) TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
}
return FConstructor(new TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
}
TAlignedBytes<sizeof(TTask),alignof(TTask)> TaskStorage; // 被執行的任務物件.
bool TaskConstructed;
FGraphEventRef Subsequents; // 後續任務同步物件.
};
- TAsyncGraphTask
上面可知TGraphTask雖然是任務,但它執行的實際任務是TTask的模板類,UE的註釋裡邊給出了TTask的基本形式:
class FGenericTask
{
TSomeType SomeArgument;
public:
FGenericTask(TSomeType InSomeArgument) // 不能用引用, 可用指標代替之.
: SomeArgument(InSomeArgument)
{
// Usually the constructor doesn't do anything except save the arguments for use in DoWork or GetDesiredThread.
}
~FGenericTask()
{
// you will be destroyed immediately after you execute. Might as well do cleanup in DoWork, but you could also use a destructor.
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FGenericTask, STATGROUP_TaskGraphTasks);
}
[static] ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::[named thread or AnyThread];
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
// The arguments are useful for setting up other tasks.
// Do work here, probably using SomeArgument.
MyCompletionGraphEvent->DontCompleteUntil(TGraphTask<FSomeChildTask>::CreateTask(NULL,CurrentThread).ConstructAndDispatchWhenReady());
}
};
然而,我們如果需要定製自己的任務,直接使用或派生TAsyncGraphTask類即可,無需另起爐灶。TAsyncGraphTask和其父類FAsyncGraphTaskBase宣告如下:
// Engine\Source\Runtime\Core\Public\Async\Async.h
// 後序任務模式
namespace ESubsequentsMode
{
enum Type
{
TrackSubsequents, // 追蹤後序任務
FireAndForget // 無需追蹤任務依賴, 可以避免執行緒同步, 提升執行效率.
};
}
class FAsyncGraphTaskBase
{
public:
TStatId GetStatId() const
{
return GET_STATID(STAT_TaskGraph_OtherTasks);
}
// 任務後序模式.
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::FireAndForget;
}
};
template<typename ResultType>
class TAsyncGraphTask : public FAsyncGraphTaskBase
{
public:
// 構造任務, InFunction就是需要執行的程式碼段.
TAsyncGraphTask(TUniqueFunction<ResultType()>&& InFunction, TPromise<ResultType>&& InPromise, ENamedThreads::Type InDesiredThread = ENamedThreads::AnyThread)
: Function(MoveTemp(InFunction))
, Promise(MoveTemp(InPromise))
, DesiredThread(InDesiredThread)
{ }
public:
// 執行任務
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
SetPromise(Promise, Function);
}
ENamedThreads::Type GetDesiredThread()
{
return DesiredThread;
}
TFuture<ResultType> GetFuture()
{
return Promise.GetFuture();
}
private:
TUniqueFunction<ResultType()> Function; // 被執行的函式物件.
TPromise<ResultType> Promise; // 同步物件.
ENamedThreads::Type DesiredThread; // 期望執行的執行緒型別.
};
- FTaskThreadBase
FTaskThreadBase是執行任務的執行緒父類,定義了一組設定、操作任務的介面,宣告如下:
class FTaskThreadBase : public FRunnable, FSingleThreadRunnable
{
public:
FTaskThreadBase()
: ThreadId(ENamedThreads::AnyThread)
, PerThreadIDTLSSlot(0xffffffff)
, OwnerWorker(nullptr)
{
NewTasks.Reset(128);
}
// 設定資料.
void Setup(ENamedThreads::Type InThreadId, uint32 InPerThreadIDTLSSlot, FWorkerThread* InOwnerWorker)
{
ThreadId = InThreadId;
check(ThreadId >= 0);
PerThreadIDTLSSlot = InPerThreadIDTLSSlot;
OwnerWorker = InOwnerWorker;
}
// 從當前執行緒初始化.
void InitializeForCurrentThread()
{
// 設定平臺相關的TLS.
FPlatformTLS::SetTlsValue(PerThreadIDTLSSlot, OwnerWorker);
}
ENamedThreads::Type GetThreadId() const;
// 用於帶名字的執行緒處理任務直到執行緒空閒或RequestQuit被呼叫.
virtual void ProcessTasksUntilQuit(int32 QueueIndex) = 0;
// 用於帶名字的執行緒處理任務直到執行緒空閒或RequestQuit被呼叫.
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex);
// 請求退出. 會導致執行緒空閒時退出到呼叫者. 如果是帶名字的執行緒, 在ProcessTasksUntilQuit中用以返回給呼叫者; 無名執行緒則直接關閉.
virtual void RequestQuit(int32 QueueIndex) = 0;
// 入隊任務, 假設this執行緒和當前執行緒一樣. 如果是帶名字的執行緒, 會直接進入私有的佇列.
virtual void EnqueueFromThisThread(int32 QueueIndex, FBaseGraphTask* Task);
// 入隊任務, 假設this執行緒和當前執行緒不一樣.
virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask* Task);
// 喚醒執行緒.
virtual void WakeUp();
// 查詢任務是否在處理中.
virtual bool IsProcessingTasks(int32 QueueIndex) = 0;
// 單執行緒幀更新
virtual void Tick() override
{
ProcessTasksUntilIdle(0);
}
// FRunnable API
virtual bool Init() override
{
InitializeForCurrentThread();
return true;
}
virtual uint32 Run() override
{
check(OwnerWorker); // make sure we are started up
ProcessTasksUntilQuit(0);
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
virtual void Stop() override
{
RequestQuit(-1);
}
virtual void Exit() override
{
}
virtual FSingleThreadRunnable* GetSingleThreadInterface() override
{
return this;
}
protected:
ENamedThreads::Type ThreadId; // 執行緒id(執行緒索引)
uint32 PerThreadIDTLSSlot; // TLS槽.
FThreadSafeCounter IsStalled; // 阻塞計數器. 用於觸發阻塞訊號.
TArray<FBaseGraphTask*> NewTasks; // 待處理的任務列表.
FWorkerThread* OwnerWorker; // 所在的工作執行緒物件.
};
FTaskThreadBase只是抽象類,具體的實現由子類FNamedTaskThread和FTaskThreadAnyThread完成。
其中FNamedTaskThread處理帶名字執行緒的任務:
// 帶名字的任務執行緒.
class FNamedTaskThread : public FTaskThreadBase
{
public:
// 用於帶名字的執行緒處理任務直到執行緒空閒或RequestQuit被呼叫.
virtual void ProcessTasksUntilQuit(int32 QueueIndex) override
{
check(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = false;
verify(++Queue(QueueIndex).RecursionGuard == 1);
// 不斷地迴圈處理佇列任務, 直到退出、關閉或平臺不支援多執行緒。
do
{
ProcessTasksNamedThread(QueueIndex, FPlatformProcess::SupportsMultithreading());
} while (!Queue(QueueIndex).QuitForReturn && !Queue(QueueIndex).QuitForShutdown && FPlatformProcess::SupportsMultithreading()); // @Hack - quit now when running with only one thread.
verify(!--Queue(QueueIndex).RecursionGuard);
}
// 用於帶名字的執行緒處理任務直到執行緒空閒或RequestQuit被呼叫.
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex) override
{
check(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = false;
verify(++Queue(QueueIndex).RecursionGuard == 1);
uint64 ProcessedTasks = ProcessTasksNamedThread(QueueIndex, false);
verify(!--Queue(QueueIndex).RecursionGuard);
return ProcessedTasks;
}
// 處理任務.
uint64 ProcessTasksNamedThread(int32 QueueIndex, bool bAllowStall)
{
uint64 ProcessedTasks = 0;
(......)
TStatId StallStatId;
bool bCountAsStall = false;
(......)
while (!Queue(QueueIndex).QuitForReturn)
{
// 從佇列首部獲取任務.
FBaseGraphTask* Task = Queue(QueueIndex).StallQueue.Pop(0, bAllowStall);
TestRandomizedThreads();
if (!Task)
{
if (bAllowStall)
{
{
FScopeCycleCounter Scope(StallStatId);
Queue(QueueIndex).StallRestartEvent->Wait(MAX_uint32, bCountAsStall);
if (Queue(QueueIndex).QuitForShutdown)
{
return ProcessedTasks;
}
TestRandomizedThreads();
}
continue;
}
else
{
break; // we were asked to quit
}
}
else // 任務不為空
{
// 執行任務.
Task->Execute(NewTasks, ENamedThreads::Type(ThreadId | (QueueIndex << ENamedThreads::QueueIndexShift)));
ProcessedTasks++;
TestRandomizedThreads();
}
}
return ProcessedTasks;
}
virtual void EnqueueFromThisThread(int32 QueueIndex, FBaseGraphTask* Task) override
{
checkThreadGraph(Task && Queue(QueueIndex).StallRestartEvent); // make sure we are started up
uint32 PriIndex = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn) ? 0 : 1;
int32 ThreadToStart = Queue(QueueIndex).StallQueue.Push(Task, PriIndex);
check(ThreadToStart < 0); // if I am stalled, then how can I be queueing a task?
}
virtual void RequestQuit(int32 QueueIndex) override
{
// this will not work under arbitrary circumstances. For example you should not attempt to stop threads unless they are known to be idle.
if (!Queue(0).StallRestartEvent)
{
return;
}
if (QueueIndex == -1)
{
// we are shutting down
checkThreadGraph(Queue(0).StallRestartEvent); // make sure we are started up
checkThreadGraph(Queue(1).StallRestartEvent); // make sure we are started up
Queue(0).QuitForShutdown = true;
Queue(1).QuitForShutdown = true;
Queue(0).StallRestartEvent->Trigger();
Queue(1).StallRestartEvent->Trigger();
}
else
{
checkThreadGraph(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = true;
}
}
virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask* Task) override
{
TestRandomizedThreads();
checkThreadGraph(Task && Queue(QueueIndex).StallRestartEvent); // make sure we are started up
uint32 PriIndex = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn) ? 0 : 1;
int32 ThreadToStart = Queue(QueueIndex).StallQueue.Push(Task, PriIndex);
if (ThreadToStart >= 0)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TaskGraph_EnqueueFromOtherThread_Trigger);
checkThreadGraph(ThreadToStart == 0);
TASKGRAPH_SCOPE_CYCLE_COUNTER(1, STAT_TaskGraph_EnqueueFromOtherThread_Trigger);
Queue(QueueIndex).StallRestartEvent->Trigger();
return true;
}
return false;
}
virtual bool IsProcessingTasks(int32 QueueIndex) override
{
return !!Queue(QueueIndex).RecursionGuard;
}
private:
// 執行緒任務佇列.
struct FThreadTaskQueue
{
FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> StallQueue; // 阻塞的任務佇列.
uint32 RecursionGuard; // 防止迴圈(遞迴)呼叫.
bool QuitForReturn; // 是否請求退出.
bool QuitForShutdown; // 是否請求關閉.
FEvent* StallRestartEvent; // 當執行緒滿載時的阻塞事件.
};
FORCEINLINE FThreadTaskQueue& Queue(int32 QueueIndex)
{
checkThreadGraph(QueueIndex >= 0 && QueueIndex < ENamedThreads::NumQueues);
return Queues[QueueIndex];
}
FORCEINLINE const FThreadTaskQueue& Queue(int32 QueueIndex) const
{
checkThreadGraph(QueueIndex >= 0 && QueueIndex < ENamedThreads::NumQueues);
return Queues[QueueIndex];
}
FThreadTaskQueue Queues[ENamedThreads::NumQueues]; // 帶名字執行緒專用的任務佇列.
};
FTaskThreadAnyThread用於處理無名執行緒的任務,由於無名執行緒有很多個,所以處理任務時和FNamedTaskThread有所不同:
class FTaskThreadAnyThread : public FTaskThreadBase
{
public:
virtual void ProcessTasksUntilQuit(int32 QueueIndex) override
{
if (PriorityIndex != (ENamedThreads::BackgroundThreadPriority >> ENamedThreads::ThreadPriorityShift))
{
FMemory::SetupTLSCachesOnCurrentThread();
}
check(!QueueIndex);
do
{
// 處理任務
ProcessTasks();
} while (!Queue.QuitForShutdown && FPlatformProcess::SupportsMultithreading()); // @Hack - quit now when running with only one thread.
}
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex) override
{
if (!FPlatformProcess::SupportsMultithreading())
{
// 處理任務
return ProcessTasks();
}
else
{
check(0);
return 0;
}
}
(......)
private:
#if UE_EXTERNAL_PROFILING_ENABLED
static inline const TCHAR* ThreadPriorityToName(int32 PriorityIdx)
{
PriorityIdx <<= ENamedThreads::ThreadPriorityShift;
if (PriorityIdx == ENamedThreads::HighThreadPriority)
{
return TEXT("Task Thread HP"); // 高優先順序的工作執行緒
}
else if (PriorityIdx == ENamedThreads::NormalThreadPriority)
{
return TEXT("Task Thread NP"); // 普通優先順序的工作執行緒
}
else if (PriorityIdx == ENamedThreads::BackgroundThreadPriority)
{
return TEXT("Task Thread BP"); // 後臺優先順序的工作執行緒
}
else
{
return TEXT("Task Thread Unknown Priority");
}
}
#endif
// 此處的處理任務與FNamedTaskThread有區別, 在於獲取任務的方式不一樣, 是從TaskGraph系統中的無名任務佇列獲取任務的.
uint64 ProcessTasks()
{
LLM_SCOPE(ELLMTag::TaskGraphTasksMisc);
TStatId StallStatId;
bool bCountAsStall = true;
uint64 ProcessedTasks = 0;
(......)
verify(++Queue.RecursionGuard == 1);
bool bDidStall = false;
while (1)
{
// 從TaskGraph系統中的無名任務佇列獲取任務的.
FBaseGraphTask* Task = FindWork();
if (!Task)
{
(......)
TestRandomizedThreads();
if (FPlatformProcess::SupportsMultithreading())
{
FScopeCycleCounter Scope(StallStatId);
Queue.StallRestartEvent->Wait(MAX_uint32, bCountAsStall);
bDidStall = true;
}
if (Queue.QuitForShutdown || !FPlatformProcess::SupportsMultithreading())
{
break;
}
TestRandomizedThreads();
(......)
continue;
}
TestRandomizedThreads();
(......)
bDidStall = false;
Task->Execute(NewTasks, ENamedThreads::Type(ThreadId));
ProcessedTasks++;
TestRandomizedThreads();
if (Queue.bStallForTuning)
{
{
FScopeLock Lock(&Queue.StallForTuning);
}
}
}
verify(!--Queue.RecursionGuard);
return ProcessedTasks;
}
// 任務佇列資料.
struct FThreadTaskQueue
{
FEvent* StallRestartEvent;
uint32 RecursionGuard;
bool QuitForShutdown;
bool bStallForTuning;
FCriticalSection StallForTuning; // 阻塞臨界區
};
// 從TaskGraph系統中獲取任務.
FBaseGraphTask* FindWork()
{
return FTaskGraphImplementation::Get().FindWork(ThreadId);
}
FThreadTaskQueue Queue; // 任務佇列, 只有第一個用於無名執行緒.
int32 PriorityIndex;
};
- ENamedThreads
在理解TaskGraph的實現和使用之前,有必要理解ENamedThreads相關的機制。ENamedThreads是一個名稱空間,此空間內提供了編解碼執行緒、優先順序的操作。它的宣告和解析如下:
namespace ENamedThreads
{
enum Type : int32
{
UnusedAnchor = -1,
// ----專用(帶名字的)執行緒----
#if STATS
StatsThread, // 統計執行緒
#endif
RHIThread, // RHI執行緒
AudioThread, // 音訊執行緒
GameThread, // 遊戲執行緒
ActualRenderingThread = GameThread + 1, // 實際渲染執行緒. GetRenderingThread()獲取的渲染可能是實際渲染執行緒也可能是遊戲執行緒.
AnyThread = 0xff, // 任意執行緒(未知執行緒, 無名執行緒)
// ----佇列索引和優先順序----
MainQueue = 0x000, // 主佇列
LocalQueue = 0x100, // 區域性佇列
NumQueues = 2,
ThreadIndexMask = 0xff,
QueueIndexMask = 0x100,
QueueIndexShift = 8,
// ----佇列任務索引、優先順序----
NormalTaskPriority = 0x000, // 普通任務優先順序
HighTaskPriority = 0x200, // 高任務優先順序
NumTaskPriorities = 2,
TaskPriorityMask = 0x200,
TaskPriorityShift = 9,
// ----執行緒優先順序----
NormalThreadPriority = 0x000, // 普通執行緒優先順序
HighThreadPriority = 0x400, // 高執行緒優先順序
BackgroundThreadPriority = 0x800, // 後臺執行緒優先順序
NumThreadPriorities = 3,
ThreadPriorityMask = 0xC00,
ThreadPriorityShift = 10,
// 組合標記
#if STATS
StatsThread_Local = StatsThread | LocalQueue,
#endif
GameThread_Local = GameThread | LocalQueue,
ActualRenderingThread_Local = ActualRenderingThread | LocalQueue,
AnyHiPriThreadNormalTask = AnyThread | HighThreadPriority | NormalTaskPriority,
AnyHiPriThreadHiPriTask = AnyThread | HighThreadPriority | HighTaskPriority,
AnyNormalThreadNormalTask = AnyThread | NormalThreadPriority | NormalTaskPriority,
AnyNormalThreadHiPriTask = AnyThread | NormalThreadPriority | HighTaskPriority,
AnyBackgroundThreadNormalTask = AnyThread | BackgroundThreadPriority | NormalTaskPriority,
AnyBackgroundHiPriTask = AnyThread | BackgroundThreadPriority | HighTaskPriority,
};
struct FRenderThreadStatics
{
private:
// 儲存了渲染執行緒,注意是原子操作型別。
static CORE_API TAtomic<Type> RenderThread;
static CORE_API TAtomic<Type> RenderThread_Local;
};
// ----設定和獲取渲染執行緒介面----
Type GetRenderThread();
Type GetRenderThread_Local();
void SetRenderThread(Type Thread);
void SetRenderThread_Local(Type Thread);
extern CORE_API int32 bHasBackgroundThreads; // 是否有後臺執行緒
extern CORE_API int32 bHasHighPriorityThreads; // 是否有高優先順序執行緒
// ----設定和獲取執行緒索引、執行緒優先順序、任務優先順序介面----
Type GetThreadIndex(Type ThreadAndIndex);
int32 GetQueueIndex(Type ThreadAndIndex);
int32 GetTaskPriority(Type ThreadAndIndex);
int32 GetThreadPriorityIndex(Type ThreadAndIndex);
Type SetPriorities(Type ThreadAndIndex, Type ThreadPriority, Type TaskPriority);
Type SetPriorities(Type ThreadAndIndex, int32 PriorityIndex, bool bHiPri);
Type SetThreadPriority(Type ThreadAndIndex, Type ThreadPriority);
Type SetTaskPriority(Type ThreadAndIndex, Type TaskPriority);
}
- FTaskGraphInterface
上面提到了很多工型別,本節才真正涉及這些任務的管理器和工廠FTaskGraphInterface。FTaskGraphInterface就是任務圖的管理者,提供了任務的操作介面:
class FTaskGraphInterface
{
virtual void QueueTask(class FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) = 0;
public:
// FTaskGraphInterface物件操作介面
static CORE_API void Startup(int32 NumThreads);
static CORE_API void Shutdown();
static CORE_API bool IsRunning();
static CORE_API FTaskGraphInterface& Get();
// 執行緒操作介面.
virtual ENamedThreads::Type GetCurrentThreadIfKnown(bool bLocalQueue = false) = 0;
virtual int32 GetNumWorkerThreads() = 0;
virtual bool IsThreadProcessingTasks(ENamedThreads::Type ThreadToCheck) = 0;
virtual void AttachToThread(ENamedThreads::Type CurrentThread)=0;
virtual uint64 ProcessThreadUntilIdle(ENamedThreads::Type CurrentThread)=0;
// 任務操作介面.
virtual void ProcessThreadUntilRequestReturn(ENamedThreads::Type CurrentThread)=0;
virtual void RequestReturn(ENamedThreads::Type CurrentThread)=0;
virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)=0;
virtual void TriggerEventWhenTasksComplete(FEvent* InEvent, const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask)=0;
void WaitUntilTaskCompletes(const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread);
void TriggerEventWhenTaskCompletes(FEvent* InEvent, const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask);
virtual void AddShutdownCallback(TFunction<void()>& Callback) = 0;
static void BroadcastSlow_OnlyUseForSpecialPurposes(bool bDoTaskThreads, bool bDoBackgroundThreads, TFunction<void(ENamedThreads::Type CurrentThread)>& Callback);
};
FTaskGraphInterface的實現是在FTaskGraphImplementation類中,FTaskGraphImplementation採用了特殊的執行緒物件WorkerThreads(工作執行緒)來作為執行的載體,當然如果是專用的(帶名字的執行緒,如GameThread、RHI、ActualRenderingThread)執行緒,則會進入專用的任務佇列。由於它的實現細節很多,後面再展開討論。
- FTaskGraphImplementation
FTaskGraphImplementation繼承並實現了FTaskGraphInterface的介面,部分介面和實現如下:
// Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp
class FTaskGraphImplementation : public FTaskGraphInterface
{
public:
static FTaskGraphImplementation& Get();
// 建構函式, 計算任務執行緒數量, 建立專用執行緒和無名執行緒等.
FTaskGraphImplementation(int32)
{
bCreatedHiPriorityThreads = !!ENamedThreads::bHasHighPriorityThreads;
bCreatedBackgroundPriorityThreads = !!ENamedThreads::bHasBackgroundThreads;
int32 MaxTaskThreads = MAX_THREADS; // 最大任務執行緒數量預設是83.
int32 NumTaskThreads = FPlatformMisc::NumberOfWorkerThreadsToSpawn(); // 根據硬體核心數量獲取任務執行緒數量.
// 處理不能支援多執行緒的平臺.
if (!FPlatformProcess::SupportsMultithreading())
{
MaxTaskThreads = 1;
NumTaskThreads = 1;
LastExternalThread = (ENamedThreads::Type)(ENamedThreads::ActualRenderingThread - 1);
bCreatedHiPriorityThreads = false;
bCreatedBackgroundPriorityThreads = false;
ENamedThreads::bHasBackgroundThreads = 0;
ENamedThreads::bHasHighPriorityThreads = 0;
}
else
{
LastExternalThread = ENamedThreads::ActualRenderingThread;
}
// 專用執行緒數量
NumNamedThreads = LastExternalThread + 1;
// 計算工作執行緒集數量, 與是否開啟執行緒高優先順序、是否建立後臺優先順序執行緒有關。
NumTaskThreadSets = 1 + bCreatedHiPriorityThreads + bCreatedBackgroundPriorityThreads;
// 計算真正需要的任務執行緒數量, 最大不超過83個.
NumThreads = FMath::Max<int32>(FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS), NumNamedThreads + 1);
NumThreads = FMath::Min(NumThreads, NumNamedThreads + NumTaskThreads * NumTaskThreadSets);
NumTaskThreadsPerSet = (NumThreads - NumNamedThreads) / NumTaskThreadSets;
ReentrancyCheck.Increment(); // just checking for reentrancy
PerThreadIDTLSSlot = FPlatformTLS::AllocTlsSlot();
// 建立所有任務執行緒.
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ThreadIndex++)
{
check(!WorkerThreads[ThreadIndex].bAttached); // reentrant?
// 根據是否專用執行緒分別建立執行緒.
bool bAnyTaskThread = ThreadIndex >= NumNamedThreads;
if (bAnyTaskThread)
{
WorkerThreads[ThreadIndex].TaskGraphWorker = new FTaskThreadAnyThread(ThreadIndexToPriorityIndex(ThreadIndex));
}
else
{
WorkerThreads[ThreadIndex].TaskGraphWorker = new FNamedTaskThread;
}
WorkerThreads[ThreadIndex].TaskGraphWorker->Setup(ENamedThreads::Type(ThreadIndex), PerThreadIDTLSSlot, &WorkerThreads[ThreadIndex]);
}
TaskGraphImplementationSingleton = this; // 賦值this到TaskGraphImplementationSingleton, 以便外部可獲取.
// 設定無名執行緒的屬性.
for (int32 ThreadIndex = LastExternalThread + 1; ThreadIndex < NumThreads; ThreadIndex++)
{
FString Name;
const ANSICHAR* GroupName = "TaskGraphNormal";
int32 Priority = ThreadIndexToPriorityIndex(ThreadIndex);
EThreadPriority ThreadPri;
uint64 Affinity = FPlatformAffinity::GetTaskGraphThreadMask();
if (Priority == 1)
{
Name = FString::Printf(TEXT("TaskGraphThreadHP %d"), ThreadIndex - (LastExternalThread + 1));
GroupName = "TaskGraphHigh";
ThreadPri = TPri_SlightlyBelowNormal; // we want even hi priority tasks below the normal threads
// If the platform defines FPlatformAffinity::GetTaskGraphHighPriorityTaskMask then use it
if (FPlatformAffinity::GetTaskGraphHighPriorityTaskMask() != 0xFFFFFFFFFFFFFFFF)
{
Affinity = FPlatformAffinity::GetTaskGraphHighPriorityTaskMask();
}
}
else if (Priority == 2)
{
Name = FString::Printf(TEXT("TaskGraphThreadBP %d"), ThreadIndex - (LastExternalThread + 1));
GroupName = "TaskGraphLow";
ThreadPri = TPri_Lowest;
// If the platform defines FPlatformAffinity::GetTaskGraphBackgroundTaskMask then use it
if ( FPlatformAffinity::GetTaskGraphBackgroundTaskMask() != 0xFFFFFFFFFFFFFFFF )
{
Affinity = FPlatformAffinity::GetTaskGraphBackgroundTaskMask();
}
}
else
{
Name = FString::Printf(TEXT("TaskGraphThreadNP %d"), ThreadIndex - (LastExternalThread + 1));
ThreadPri = TPri_BelowNormal; // we want normal tasks below normal threads like the game thread
}
// 計算執行緒棧大小.
#if WITH_EDITOR
uint32 StackSize = 1024 * 1024;
#elif ( UE_BUILD_SHIPPING || UE_BUILD_TEST )
uint32 StackSize = 384 * 1024;
#else
uint32 StackSize = 512 * 1024;
#endif
// 真正地建立工作執行緒的執行執行緒.
WorkerThreads[ThreadIndex].RunnableThread = FRunnableThread::Create(&Thread(ThreadIndex), *Name, StackSize, ThreadPri, Affinity); // these are below normal threads so that they sleep when the named threads are active
WorkerThreads[ThreadIndex].bAttached = true;
if (WorkerThreads[ThreadIndex].RunnableThread)
{
TRACE_SET_THREAD_GROUP(WorkerThreads[ThreadIndex].RunnableThread->GetThreadID(), GroupName);
}
}
}
// 入隊任務.
virtual void QueueTask(FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type InCurrentThreadIfKnown = ENamedThreads::AnyThread) final override
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(2, STAT_TaskGraph_QueueTask);
if (ENamedThreads::GetThreadIndex(ThreadToExecuteOn) == ENamedThreads::AnyThread)
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(3, STAT_TaskGraph_QueueTask_AnyThread);
// 多執行緒支援下的處理.
if (FPlatformProcess::SupportsMultithreading())
{
// 處理優先順序.
uint32 TaskPriority = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn);
int32 Priority = ENamedThreads::GetThreadPriorityIndex(Task->ThreadToExecuteOn);
if (Priority == (ENamedThreads::BackgroundThreadPriority >> ENamedThreads::ThreadPriorityShift) && (!bCreatedBackgroundPriorityThreads || !ENamedThreads::bHasBackgroundThreads))
{
Priority = ENamedThreads::NormalThreadPriority >> ENamedThreads::ThreadPriorityShift; // we don't have background threads, promote to normal
TaskPriority = ENamedThreads::NormalTaskPriority >> ENamedThreads::TaskPriorityShift; // demote to normal task pri
}
else if (Priority == (ENamedThreads::HighThreadPriority >> ENamedThreads::ThreadPriorityShift) && (!bCreatedHiPriorityThreads || !ENamedThreads::bHasHighPriorityThreads))
{
Priority = ENamedThreads::NormalThreadPriority >> ENamedThreads::ThreadPriorityShift; // we don't have hi priority threads, demote to normal
TaskPriority = ENamedThreads::HighTaskPriority >> ENamedThreads::TaskPriorityShift; // promote to hi task pri
}
uint32 PriIndex = TaskPriority ? 0 : 1;
check(Priority >= 0 && Priority < MAX_THREAD_PRIORITIES);
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(4, STAT_TaskGraph_QueueTask_IncomingAnyThreadTasks_Push);
// 將任務壓入待執行佇列, 且獲得並執行可執行的任務索引(可能無).
int32 IndexToStart = IncomingAnyThreadTasks[Priority].Push(Task, PriIndex);
if (IndexToStart >= 0)
{
StartTaskThread(Priority, IndexToStart);
}
}
return;
}
else
{
ThreadToExecuteOn = ENamedThreads::GameThread;
}
}
// 以下是不支援多執行緒的處理.
ENamedThreads::Type CurrentThreadIfKnown;
if (ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown) == ENamedThreads::AnyThread)
{
CurrentThreadIfKnown = GetCurrentThread();
}
else
{
CurrentThreadIfKnown = ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown);
checkThreadGraph(CurrentThreadIfKnown == ENamedThreads::GetThreadIndex(GetCurrentThread()));
}
{
int32 QueueToExecuteOn = ENamedThreads::GetQueueIndex(ThreadToExecuteOn);
ThreadToExecuteOn = ENamedThreads::GetThreadIndex(ThreadToExecuteOn);
FTaskThreadBase* Target = &Thread(ThreadToExecuteOn);
if (ThreadToExecuteOn == ENamedThreads::GetThreadIndex(CurrentThreadIfKnown))
{
Target->EnqueueFromThisThread(QueueToExecuteOn, Task);
}
else
{
Target->EnqueueFromOtherThread(QueueToExecuteOn, Task);
}
}
}
virtual int32 GetNumWorkerThreads() final override;
virtual ENamedThreads::Type GetCurrentThreadIfKnown(bool bLocalQueue) final override;
virtual bool IsThreadProcessingTasks(ENamedThreads::Type ThreadToCheck) final override;
// 將當前執行緒匯入到指定Index.
virtual void AttachToThread(ENamedThreads::Type CurrentThread) final override;
// ----處理任務介面----
virtual uint64 ProcessThreadUntilIdle(ENamedThreads::Type CurrentThread) final override;
virtual void ProcessThreadUntilRequestReturn(ENamedThreads::Type CurrentThread) final override;
virtual void RequestReturn(ENamedThreads::Type CurrentThread) final override;
virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) final override;
virtual void TriggerEventWhenTasksComplete(FEvent* InEvent, const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask) final override;
virtual void AddShutdownCallback(TFunction<void()>& Callback);
// ----任務排程介面----
// 開啟指定優先順序和索引的任務執行緒.
void StartTaskThread(int32 Priority, int32 IndexToStart);
void StartAllTaskThreads(bool bDoBackgroundThreads);
FBaseGraphTask* FindWork(ENamedThreads::Type ThreadInNeed);
void StallForTuning(int32 Index, bool Stall);
void SetTaskThreadPriorities(EThreadPriority Pri);
private:
// 獲取指定索引的任務執行緒引用.
FTaskThreadBase& Thread(int32 Index)
{
checkThreadGraph(Index >= 0 && Index < NumThreads);
checkThreadGraph(WorkerThreads[Index].TaskGraphWorker->GetThreadId() == Index);
return *WorkerThreads[Index].TaskGraphWorker;
}
// 獲取當前執行緒索引.
ENamedThreads::Type GetCurrentThread();
int32 ThreadIndexToPriorityIndex(int32 ThreadIndex);
enum
{
MAX_THREADS = 26 * (CREATE_HIPRI_TASK_THREADS + CREATE_BACKGROUND_TASK_THREADS + 1) + ENamedThreads::ActualRenderingThread + 1,
MAX_THREAD_PRIORITIES = 3
};
FWorkerThread WorkerThreads[MAX_THREADS]; // 所有工作執行緒(任務執行緒)物件陣列.
int32 NumThreads; // 實際上被使用的執行緒數量.
int32 NumNamedThreads; // 專用執行緒數量.
int32 NumTaskThreadSets;// 任務執行緒集合數量.
int32 NumTaskThreadsPerSet; // 每個集合擁有的任務執行緒數量.
bool bCreatedHiPriorityThreads;
bool bCreatedBackgroundPriorityThreads;
ENamedThreads::Type LastExternalThread;
FThreadSafeCounter ReentrancyCheck;
uint32 PerThreadIDTLSSlot;
TArray<TFunction<void()> > ShutdownCallbacks; // 銷燬前的回撥.
FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> IncomingAnyThreadTasks[MAX_THREAD_PRIORITIES];
};
總結起來,TaskGraph會根據執行緒優先順序、是否啟用後臺執行緒建立不同的工作執行緒集合,然後建立它們的FWorkerThread物件。入隊任務時,會將任務Push到任務列表IncomingAnyThreadTasks(型別是FStallingTaskQueue,執行緒安全的無鎖的連結串列)中,並取出可執行的任務索引,根據任務的屬性(希望在哪個執行緒執行、優先順序、任務索引)啟用對應的工作執行緒去執行。
TaskGraph涉及的工作執行緒FWorkerThread宣告如下:
struct FWorkerThread
{
FTaskThreadBase* TaskGraphWorker; // 所在的FTaskThread物件(被FTaskThread物件擁有)
FRunnableThread* RunnableThread; // 真正執行任務的可執行執行緒.
bool bAttached; // 是否附加的執行緒.(一般用於專用執行緒)
};
由此可見,TaskGraph最終也是藉助FRunnableThread來執行任務。TaskGraph系統總算是和FRunnableThread聯絡起來,形成了閉環。
至此,終於將TaskGraph體系的主幹脈絡闡述完了,當然,還有很多技術細節(如同步事件、觸發細節、排程演算法、無鎖連結串列以及部分概念)並沒有涉及,這些就留給讀者自己去研讀UE原始碼探索了。
2.5 UE的多執行緒渲染
前面做了大量的基礎鋪墊,終於回到了主題,講UE的多執行緒渲染相關的知識。
2.5.1 UE的多執行緒渲染基礎
2.5.1.1 場景和渲染模組主要型別
UE的場景和渲染模組涉及到概念非常多,主要型別和解析如下:
型別 | 解析 |
---|---|
UWorld | 包含了一組可以相互互動的Actor和元件的集合,多個關卡(Level)可以被載入進UWorld或從UWorld解除安裝。可以同時存在多個UWorld例項。 |
ULevel | 關卡,儲存著一組Actor和元件,並且儲存在同一個檔案。 |
USceneComponent | 場景元件,是所有可以被加入到場景的物體的父類,比如燈光、模型、霧等。 |
UPrimitiveComponent | 圖元元件,是所有可渲染或擁有物理模擬的物體父類。是CPU層裁剪的最小粒度單位, |
ULightComponent | 光源元件,是所有光源型別的父類。 |
FScene | 是UWorld在渲染模組的代表。只有加入到FScene的物體才會被渲染器感知到。渲染執行緒擁有FScene的所有狀態(遊戲執行緒不可直接修改)。 |
FPrimitiveSceneProxy | 圖元場景代理,是UPrimitiveComponent在渲染器的代表,映象了UPrimitiveComponent在渲染執行緒的狀態。 |
FPrimitiveSceneInfo | 渲染器內部狀態(描述了FRendererModule的實現),相當於融合了UPrimitiveComponent and FPrimitiveSceneProxy。只存在渲染器模組,所以引擎模組無法感知到它的存在。 |
FSceneView | 描述了FScene內的單個檢視(view),同個FScene允許有多個view,換言之,一個場景可以被多個view繪製,或者多個view同時被繪製。每一幀都會建立新的view例項。 |
FViewInfo | view在渲染器的內部代表,只存在渲染器模組,引擎模組不可見。 |
FSceneViewState | 儲存了有關view的渲染器私有資訊,這些資訊需要被跨幀訪問。在Game例項,每個ULocalPlayer擁有一個FSceneViewState例項。 |
FSceneRenderer | 每幀都會被建立,封裝幀間臨時資料。下派生FDeferredShadingSceneRenderer(延遲著色場景渲染器)和FMobileSceneRenderer(移動端場景渲染器),分別代表PC和移動端的預設渲染器。 |
2.5.1.2 引擎模組和渲染模組代表
UE為了結構清晰,減少模組之間的依賴,加速迭代速度,劃分了很多模組,最主要的有引擎模組、渲染器模組、核心、RHI、外掛等等。上一小節提到了很多概念和型別,它們有些存在於引擎模組(Engine Module),有些存在於渲染器模組(Renderer Module),具體如下表:
Engine Module | Renderer Module |
---|---|
UWorld | FScene |
UPrimitiveComponent / FPrimitiveSceneProxy | FPrimitiveSceneInfo |
FSceneView | FViewInfo |
ULocalPlayer | FSceneViewState |
ULightComponent / FLightSceneProxy | FLightSceneInfo |
2.5.1.3 遊戲執行緒和渲染執行緒代表
遊戲執行緒的物件通常做邏輯更新,在記憶體中有一份持久的資料,為了避免遊戲執行緒和渲染執行緒產生競爭條件,會在渲染執行緒額外儲存一份記憶體拷貝,並且使用的是另外的型別,以下是UE比較常見的型別對映關係(遊戲執行緒物件以U開頭,渲染執行緒以F開頭):
Game Thread | Rendering Thread |
---|---|
UWorld | FScene |
UPrimitiveComponent | FPrimitiveSceneProxy / FPrimitiveSceneInfo |
- | FSceneView / FViewInfo |
ULocalPlayer | FSceneViewState |
ULightComponent | FLightSceneProxy / FLightSceneInfo |
遊戲執行緒代表一般由遊戲遊戲執行緒操作,渲染執行緒代表主要由渲染執行緒操作。如果嘗試跨執行緒運算元據,將會引發不可預料的結果,產生競爭條件。
/** SceneProxy在註冊進場景時,會在遊戲執行緒中被構造和傳遞資料。 */
FStaticMeshSceneProxy::FStaticMeshSceneProxy(UStaticMeshComponent* InComponent):
FPrimitiveSceneProxy(...),
Owner(InComponent->GetOwner()) <======== 此處將AActor指標被快取
...
/** SceneProxy的DrawDynamicElements將被渲染器在渲染執行緒中呼叫 */
void FStaticMeshSceneProxy::DrawDynamicElements(...)
{
if (Owner->AnyProperty) <========== 將會引發競爭條件! 遊戲執行緒擁有AActor、UObject的所有狀態!!並且UObject物件可能被GC掉,此時再訪問會引起程式崩潰!!
}
部分代表比較特殊,如FPrimitiveSceneProxy、FLightSceneProxy ,這些場景代理本屬於引擎模組,但又屬於渲染執行緒專屬物件,說明它們是連線遊戲執行緒和渲染執行緒的橋樑,是執行緒間傳遞資料的工具人。
2.5.2 UE的多執行緒渲染總覽
預設情況下,UE存在遊戲執行緒(Game Thread)、渲染執行緒(Render Thread)、RHI執行緒(RHI Thread),它們都獨立地執行在專門的執行緒上(FRunnableThread)。
遊戲執行緒通過某些介面向渲染執行緒的Queue入隊回撥介面,以便渲染執行緒稍後執行時,從渲染執行緒的Queue獲取回撥,一個個地執行,從而生成了Command List。
渲染執行緒作為前端(frontend)產生的Command List是平臺無關的,是抽象的圖形API呼叫;而RHI執行緒作為後端(backtend)會執行和轉換渲染執行緒的Command List成為指定圖形API的呼叫(稱為Graphical Command),並提交到GPU執行。這些執行緒處理的資料通常是不同幀的,譬如遊戲執行緒處理N幀資料,渲染執行緒和RHI執行緒處理N-1幀資料。
但也存在例外,比如渲染執行緒和RHI執行緒執行很快,幾乎不存在延遲,這種情況下,遊戲執行緒處理N幀,而渲染執行緒可能處理N或N-1幀,RHI執行緒也可能在轉換N或N-1幀。但是,渲染執行緒不能落後遊戲執行緒一幀,否則遊戲執行緒會卡住,直到渲染執行緒處理所有指令。
除此之外,渲染指令是可以並行地被生成,RHI執行緒也可以並行地轉換這些指令,如下所示:
UE4並行生成Command list示意圖。
開啟多執行緒渲染帶來的收益是幀率更高,幀間變化頻率降低(幀率更穩定)。以Fortnite(堡壘之夜)移動端為例,在開啟RHI執行緒之前,渲染執行緒急劇地上下波動,而加了RHI執行緒之後,波動平緩許多,和遊戲執行緒基本保持一致,幀率也提升不少:
2.5.3 遊戲執行緒和渲染執行緒的實現
2.5.3.1 遊戲執行緒的實現
遊戲執行緒被稱為主執行緒,是引擎執行的心臟,承載主要的遊戲邏輯、執行流程的工作,也是其它執行緒的資料發起者。
遊戲執行緒的建立是執行程式入口的執行緒,由系統啟動程式時被同時建立的(因為程式至少需要一個執行緒來工作),在引擎啟動時直接儲存到全域性變數中,且稍後會設定到TaskGraph系統中:
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
// 獲取當前執行緒id, 儲存到全域性變數中.
GGameThreadId = FPlatformTLS::GetCurrentThreadId();
GIsGameThreadIdInitialized = true;
FPlatformProcess::SetThreadAffinityMask(FPlatformAffinity::GetMainGameMask());
// 設定遊戲執行緒資料(但很多平臺都是空的實現體)
FPlatformProcess::SetupGameThread();
(......)
if (bCreateTaskGraphAndThreadPools)
{
SCOPED_BOOT_TIMING("FTaskGraphInterface::Startup");
FTaskGraphInterface::Startup(FPlatformMisc::NumberOfCores());
// 將當前執行緒(主執行緒)附加到TaskGraph的GameThread命名插槽中. 這樣主執行緒便和TaskGraph聯動了起來.
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread);
}
}
以上程式碼也說明:主執行緒、遊戲執行緒和TaskGraph系統的ENamedThreads::GameThread其實是一回事,都是同一個執行緒!
經過上面的初始化和設定後,其它地方就可以通過TaskGraph系統並行地處理任務了,也可以訪問全域性變數,以便判斷遊戲執行緒是否初始化完,當前執行緒是否遊戲執行緒:
bool IsInGameThread()
{
return GIsGameThreadIdInitialized && FPlatformTLS::GetCurrentThreadId() == GGameThreadId;
}
2.5.3.2 渲染執行緒的實現
渲染執行緒與遊戲不同,是一條專門用於生成渲染指令和渲染邏輯的獨立執行緒。RenderingThread.h宣告瞭全部對外的介面,部分如下:
// Engine\Source\Runtime\RenderCore\Public\RenderingThread.h
// 是否啟用了獨立的渲染執行緒, 如果為false, 則所有渲染命令會被立即執行, 而不是放入渲染命令佇列.
extern RENDERCORE_API bool GIsThreadedRendering;
// 渲染執行緒是否應該被建立. 通常被命令列引數或ToggleRenderingThread控制檯引數設定.
extern RENDERCORE_API bool GUseThreadedRendering;
// 是否開啟RHI執行緒
extern RENDERCORE_API void SetRHIThreadEnabled(bool bEnableDedicatedThread, bool bEnableRHIOnTaskThreads);
(......)
// 開啟渲染執行緒.
extern RENDERCORE_API void StartRenderingThread();
// 停止渲染執行緒.
extern RENDERCORE_API void StopRenderingThread();
// 檢查渲染執行緒是否健康(是否Crash), 如果crash, 則會用UE_Log輸出日誌.
extern RENDERCORE_API void CheckRenderingThreadHealth();
// 檢查渲染執行緒是否健康(是否Crash)
extern RENDERCORE_API bool IsRenderingThreadHealthy();
// 增加一個必須在下一個場景繪製前或flush渲染命令前完成的任務.
extern RENDERCORE_API void AddFrameRenderPrerequisite(const FGraphEventRef& TaskToAdd);
// 手機幀渲染前序任務, 保證所有渲染命令被入隊.
extern RENDERCORE_API void AdvanceFrameRenderPrerequisite();
// 等待所有渲染執行緒的渲染命令被執行完畢. 會卡住遊戲執行緒, 只能被遊戲執行緒呼叫.
extern RENDERCORE_API void FlushRenderingCommands(bool bFlushDeferredDeletes = false);
extern RENDERCORE_API void FlushPendingDeleteRHIResources_GameThread();
extern RENDERCORE_API void FlushPendingDeleteRHIResources_RenderThread();
extern RENDERCORE_API void TickRenderingTickables();
extern RENDERCORE_API void StartRenderCommandFenceBundler();
extern RENDERCORE_API void StopRenderCommandFenceBundler();
(......)
RenderingThread.h還有一個非常重要的巨集ENQUEUE_RENDER_COMMAND
,它的作用是向渲染執行緒入隊渲染指令。下面是它的宣告和實現:
// 向渲染執行緒入隊渲染指令, Type指明瞭渲染操作的名字.
#define ENQUEUE_RENDER_COMMAND(Type) \
struct Type##Name \
{ \
static const char* CStr() { return #Type; } \
static const TCHAR* TStr() { return TEXT(#Type); } \
}; \
EnqueueUniqueRenderCommand<Type##Name>
上面最後一句使用了EnqueueUniqueRenderCommand
命令,繼續追蹤之:
// TSTR是渲染命令名字, LAMBDA是回撥函式.
template<typename TSTR, typename LAMBDA>
FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LAMBDA&& Lambda)
{
typedef TEnqueueUniqueRenderCommandType<TSTR, LAMBDA> EURCType;
// 如果在渲染執行緒內直接執行回撥而不入隊渲染命令.
if (IsInRenderingThread())
{
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
else
{
// 需要在獨立的渲染執行緒執行
if (ShouldExecuteOnRenderThread())
{
CheckNotBlockedOnRenderThread();
// 從GraphTask建立任務且在適當時候入隊渲染命令.
TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
}
else // 不在獨立的渲染執行緒執行, 則直接執行.
{
EURCType TempCommand(Forward<LAMBDA>(Lambda));
FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());
TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());
}
}
}
上面說明如果是有獨立的渲染執行緒,最終會將渲染命令入隊到TaskGraph的任務Queue中,等待合適的時機在渲染執行緒中被執行。其中TEnqueueUniqueRenderCommandType
就是專用於渲染命令的特殊TaskGraph任務型別,宣告如下:
class RENDERCORE_API FRenderCommand
{
public:
// 所有渲染指令都必須在渲染執行緒執行.
static ENamedThreads::Type GetDesiredThread()
{
check(!GIsThreadedRendering || ENamedThreads::GetRenderThread() != ENamedThreads::GameThread);
return ENamedThreads::GetRenderThread();
}
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::FireAndForget;
}
};
template<typename TSTR, typename LAMBDA>
class TEnqueueUniqueRenderCommandType : public FRenderCommand
{
public:
TEnqueueUniqueRenderCommandType(LAMBDA&& InLambda) : Lambda(Forward<LAMBDA>(InLambda)) {}
// 正在執行任務.
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
TRACE_CPUPROFILER_EVENT_SCOPE_ON_CHANNEL_STR(TSTR::TStr(), RenderCommandsChannel);
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
(......)
private:
LAMBDA Lambda; // 快取渲染回撥函式.
};
為了更好理解入隊渲染命令操作,舉個具體的例子,以增加燈光到場景為例:
void FScene::AddLight(ULightComponent* Light)
{
(......)
// Send a command to the rendering thread to add the light to the scene.
FScene* Scene = this;
FLightSceneInfo* LightSceneInfo = Proxy->LightSceneInfo;
// 這裡入隊渲染指令, 以便在渲染執行緒將燈光資料傳遞到渲染器.
ENQUEUE_RENDER_COMMAND(FAddLightCommand)(
[Scene, LightSceneInfo](FRHICommandListImmediate& RHICmdList)
{
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Scene_AddLight);
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
Scene->AddLightSceneInfo_RenderThread(LightSceneInfo);
});
}
將ENQUEUE_RENDER_COMMAND(FAddLightCommand)
代入前面解析過的巨集和模板,並展開,完整的程式碼如下:
struct FAddLightCommandName
{
static const char* CStr() { return "FAddLightCommand"; }
static const TCHAR* TStr() { return TEXT("FAddLightCommand"); }
};
EnqueueUniqueRenderCommand<FAddLightCommandName>(
[Scene, LightSceneInfo](FRHICommandListImmediate& RHICmdList)
{
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Scene_AddLight);
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
Scene->AddLightSceneInfo_RenderThread(LightSceneInfo);
})
{
typedef TEnqueueUniqueRenderCommandType<FAddLightCommandName, LAMBDA> EURCType;
// 如果在渲染執行緒內直接執行回撥而不入隊渲染命令.
if (IsInRenderingThread())
{
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
else
{
// 需要在獨立的渲染執行緒執行
if (ShouldExecuteOnRenderThread())
{
CheckNotBlockedOnRenderThread();
// 從GraphTask建立任務且在適當時候入隊渲染命令.
TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
}
else // 不在獨立的渲染執行緒執行, 則直接執行.
{
EURCType TempCommand(Forward<LAMBDA>(Lambda));
FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());
TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());
}
}
}
FRenderingThread承載了渲染執行緒的主要工作,它的部分介面和實現程式碼如下:
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
class FRenderingThread : public FRunnable
{
private:
bool bAcquiredThreadOwnership; // 當沒有獨立的RHI執行緒時, 渲染執行緒將被其它執行緒捕獲.
public:
FEvent* TaskGraphBoundSyncEvent; // TaskGraph同步事件, 以便在主執行緒使用渲染執行緒之前就將渲染執行緒繫結到TaskGraph體系中.
FRenderingThread()
{
bAcquiredThreadOwnership = false;
// 獲取同步事件.
TaskGraphBoundSyncEvent = FPlatformProcess::GetSynchEventFromPool(true);
RHIFlushResources();
}
// FRunnable interface.
virtual bool Init(void) override
{
// 獲取當前執行緒ID到全域性變數GRenderThreadId, 以便其它地方引用.
GRenderThreadId = FPlatformTLS::GetCurrentThreadId();
// 處理執行緒捕獲關係.
if (!IsRunningRHIInSeparateThread())
{
bAcquiredThreadOwnership = true;
RHIAcquireThreadOwnership();
}
return true;
}
(......)
virtual uint32 Run(void) override
{
// 設定TLS.
FMemory::SetupTLSCachesOnCurrentThread();
// 設定渲染執行緒平臺相關的資料.
FPlatformProcess::SetupRenderThread();
(......)
{
// 進入渲染執行緒主迴圈.
RenderingThreadMain( TaskGraphBoundSyncEvent );
}
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
};
可見它在執行之後會進入渲染執行緒邏輯,這裡再進入RenderingThreadMain程式碼一探究竟:
void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent )
{
LLM_SCOPE(ELLMTag::RenderingThreadMemory);
// 將渲染執行緒和區域性執行緒執行緒插槽設定成ActualRenderingThread和ActualRenderingThread_Local.
ENamedThreads::Type RenderThread = ENamedThreads::Type(ENamedThreads::ActualRenderingThread);
ENamedThreads::SetRenderThread(RenderThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::Type(ENamedThreads::ActualRenderingThread_Local));
// 將當前執行緒附加到TaskGraph的RenderThread插槽中.
FTaskGraphInterface::Get().AttachToThread(RenderThread);
FPlatformMisc::MemoryBarrier();
// 觸發同步事件, 通知主執行緒渲染執行緒已經附加到TaskGraph, 已經準備好接收任務.
if( TaskGraphBoundSyncEvent != NULL )
{
TaskGraphBoundSyncEvent->Trigger();
}
(......)
// 渲染執行緒不同階段的處理.
FCoreDelegates::PostRenderingThreadCreated.Broadcast();
check(GIsThreadedRendering);
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(RenderThread);
FPlatformMisc::MemoryBarrier();
check(!GIsThreadedRendering);
FCoreDelegates::PreRenderingThreadDestroyed.Broadcast();
(......)
// 恢復執行緒執行緒到遊戲執行緒.
ENamedThreads::SetRenderThread(ENamedThreads::GameThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::GameThread_Local);
FPlatformMisc::MemoryBarrier();
}
不過這裡還留有一個很大的疑問,那就是FRenderingThread只是獲取當前執行緒作為渲染執行緒並附加到TaskGraph中,並沒有建立執行緒。那麼是哪裡建立的渲染執行緒呢?繼續追蹤,結果發現是在StartRenderingThread()
介面中建立了FRenderingThread例項,它的實現程式碼如下(節選):
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
void StartRenderingThread()
{
(......)
// Turn on the threaded rendering flag.
GIsThreadedRendering = true;
// 建立FRenderingThread例項.
GRenderingThreadRunnable = new FRenderingThread();
// 建立渲染執行緒!!
GRenderingThread = FRunnableThread::Create(GRenderingThreadRunnable, *BuildRenderingThreadName(ThreadCount), 0, FPlatformAffinity::GetRenderingThreadPriority(), FPlatformAffinity::GetRenderingThreadMask(), FPlatformAffinity::GetRenderingThreadFlags());
(......)
// 開啟渲染命令的柵欄.
FRenderCommandFence Fence;
Fence.BeginFence();
Fence.Wait();
(......)
}
如果繼續追蹤,會發現StartRenderingThread()
是在FEngineLoop::PreInitPostStartupScreen
中呼叫的。
至此,渲染執行緒的建立、初始化以及主要介面的實現都剖析完了。
2.5.3.3 RHI執行緒的實現
RHI執行緒的工作是轉換渲染指令到指定圖形API,建立、上傳渲染資源到GPU。它的主要邏輯在FRHIThread中,實現程式碼如下:
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
class FRHIThread : public FRunnable
{
public:
FRunnableThread* Thread; // 所在的RHI執行緒.
FRHIThread()
: Thread(nullptr)
{
check(IsInGameThread());
}
void Start()
{
// 開始時建立RHI執行緒.
Thread = FRunnableThread::Create(this, TEXT("RHIThread"), 512 * 1024, FPlatformAffinity::GetRHIThreadPriority(),
FPlatformAffinity::GetRHIThreadMask(), FPlatformAffinity::GetRHIThreadFlags()
);
check(Thread);
}
virtual uint32 Run() override
{
LLM_SCOPE(ELLMTag::RHIMisc);
// 初始化TLS
FMemory::SetupTLSCachesOnCurrentThread();
// 將FRHIThread所在的RHI執行緒附加到askGraph體系中,並指定到ENamedThreads::RHIThread。
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RHIThread);
// 啟動RHI執行緒,直到執行緒返回。
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(ENamedThreads::RHIThread);
// 清理TLS.
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
// 單例介面。
static FRHIThread& Get()
{
static FRHIThread Singleton; // 使用了區域性靜態變數,可以保證執行緒安全。
return Singleton;
}
};
可見RHI執行緒不同於渲染執行緒,是直接在FRHIThread物件內建立實際的執行緒。而FRHIThread的建立也是在StartRenderingThread()
中:
void StartRenderingThread()
{
(......)
if (GUseRHIThread_InternalUseOnly)
{
FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
if (!FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::RHIThread))
{
// 建立FRHIThread例項並啟動它.
FRHIThread::Get().Start();
}
DECLARE_CYCLE_STAT(TEXT("Wait For RHIThread"), STAT_WaitForRHIThread, STATGROUP_TaskGraphTasks);
// 建立RHI執行緒擁有者捕獲任務, 讓遊戲執行緒等待.
FGraphEventRef CompletionEvent = TGraphTask<FOwnershipOfRHIThreadTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady(true, GET_STATID(STAT_WaitForRHIThread));
QUICK_SCOPE_CYCLE_COUNTER(STAT_StartRenderingThread);
// 讓遊戲執行緒或區域性執行緒等待RHI執行緒處理(捕獲了執行緒擁有者, 大多數圖形API為空)完畢.
FTaskGraphInterface::Get().WaitUntilTaskCompletes(CompletionEvent, ENamedThreads::GameThread_Local);
// 儲存RHI執行緒id.
GRHIThread_InternalUseOnly = FRHIThread::Get().Thread;
check(GRHIThread_InternalUseOnly);
GIsRunningRHIInDedicatedThread_InternalUseOnly = true;
GIsRunningRHIInSeparateThread_InternalUseOnly = true;
GRHIThreadId = GRHIThread_InternalUseOnly->GetThreadID();
GRHICommandList.LatchBypass();
}
(......)
}
那麼渲染執行緒如何向RHI執行緒入隊任務呢?答案就在RHICommandList.h中:
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
// RHI命令父類
struct FRHICommandBase
{
FRHICommandBase* Next = nullptr; // 指向下一條RHI命令.
// 執行RHI命令並銷燬.
virtual void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& DebugContext) = 0;
};
// RHI命令結構體
template<typename TCmd, typename NameType = FUnnamedRhiCommand>
struct FRHICommand : public FRHICommandBase
{
(......)
void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& Context) override final
{
(......)
TCmd *ThisCmd = static_cast<TCmd*>(this);
ThisCmd->Execute(CmdList);
ThisCmd->~TCmd();
}
};
// 向RHI執行緒傳送RHI命令的巨集.
#define FRHICOMMAND_MACRO(CommandName) \
struct PREPROCESSOR_JOIN(CommandName##String, __LINE__) \
{ \
static const TCHAR* TStr() { return TEXT(#CommandName); } \
}; \
struct CommandName final : public FRHICommand<CommandName, PREPROCESSOR_JOIN(CommandName##String, __LINE__)>
RHI執行緒的相關實現機制跟渲染執行緒型別,且更加簡潔。以下是它的使用示範:
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
FRHICOMMAND_MACRO(FRHICommandDrawPrimitive)
{
uint32 BaseVertexIndex;
uint32 NumPrimitives;
uint32 NumInstances;
FORCEINLINE_DEBUGGABLE FRHICommandDrawPrimitive(uint32 InBaseVertexIndex, uint32 InNumPrimitives, uint32 InNumInstances)
: BaseVertexIndex(InBaseVertexIndex)
, NumPrimitives(InNumPrimitives)
, NumInstances(InNumInstances)
{
}
RHI_API void Execute(FRHICommandListBase& CmdList);
};
// Engine\Source\Runtime\RHI\Public\RHICommandListCommandExecutes.inl
void FRHICommandDrawPrimitive::Execute(FRHICommandListBase& CmdList)
{
RHISTAT(DrawPrimitive);
INTERNAL_DECORATOR(RHIDrawPrimitive)(BaseVertexIndex, NumPrimitives, NumInstances);
}
由此可見,所有的RHI指令都是預先宣告並實現好的,目前存在的RHI渲染指令型別達到近百種(如下),渲染執行緒建立這些宣告好的RHI指令即可在合適的被推入RHI執行緒佇列並被執行。
FRHICOMMAND_MACRO(FRHICommandUpdateGeometryCacheBuffer)
FRHICOMMAND_MACRO(FRHISubmitFrameToEncoder)
FRHICOMMAND_MACRO(FLocalRHICommand)
FRHICOMMAND_MACRO(FRHISetSpectatorScreenTexture)
FRHICOMMAND_MACRO(FRHISetSpectatorScreenModeTexturePlusEyeLayout)
FRHICOMMAND_MACRO(FRHISyncFrameCommand)
FRHICOMMAND_MACRO(FRHICommandStat)
FRHICOMMAND_MACRO(FRHICommandRHIThreadFence)
FRHICOMMAND_MACRO(FRHIAsyncComputeSubmitList)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitSubListParallel)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitSubList)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitRTSubList)
FRHICOMMAND_MACRO(FRHICommandSubmitSubList)
FRHICOMMAND_MACRO(FRHICommandBeginUpdateMultiFrameResource)
FRHICOMMAND_MACRO(FRHICommandEndUpdateMultiFrameResource)
FRHICOMMAND_MACRO(FRHICommandBeginUpdateMultiFrameUAV)
FRHICOMMAND_MACRO(FRHICommandEndUpdateMultiFrameUAV)
FRHICOMMAND_MACRO(FRHICommandSetGPUMask)
FRHICOMMAND_MACRO(FRHICommandWaitForTemporalEffect)
FRHICOMMAND_MACRO(FRHICommandBroadcastTemporalEffect)
FRHICOMMAND_MACRO(FRHICommandSetStencilRef)
FRHICOMMAND_MACRO(FRHICommandDrawPrimitive)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitive)
FRHICOMMAND_MACRO(FRHICommandSetBlendFactor)
FRHICOMMAND_MACRO(FRHICommandSetStreamSource)
FRHICOMMAND_MACRO(FRHICommandSetViewport)
FRHICOMMAND_MACRO(FRHICommandSetStereoViewport)
FRHICOMMAND_MACRO(FRHICommandSetScissorRect)
FRHICOMMAND_MACRO(FRHICommandSetRenderTargets)
FRHICOMMAND_MACRO(FRHICommandBeginRenderPass)
FRHICOMMAND_MACRO(FRHICommandEndRenderPass)
FRHICOMMAND_MACRO(FRHICommandNextSubpass)
FRHICOMMAND_MACRO(FRHICommandBeginParallelRenderPass)
FRHICOMMAND_MACRO(FRHICommandEndParallelRenderPass)
FRHICOMMAND_MACRO(FRHICommandBeginRenderSubPass)
FRHICOMMAND_MACRO(FRHICommandEndRenderSubPass)
FRHICOMMAND_MACRO(FRHICommandBeginComputePass)
FRHICOMMAND_MACRO(FRHICommandEndComputePass)
FRHICOMMAND_MACRO(FRHICommandBindClearMRTValues)
FRHICOMMAND_MACRO(FRHICommandSetGraphicsPipelineState)
FRHICOMMAND_MACRO(FRHICommandAutomaticCacheFlushAfterComputeShader)
FRHICOMMAND_MACRO(FRHICommandFlushComputeShaderCache)
FRHICOMMAND_MACRO(FRHICommandDrawPrimitiveIndirect)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedIndirect)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitiveIndirect)
FRHICOMMAND_MACRO(FRHICommandSetDepthBounds)
FRHICOMMAND_MACRO(FRHICommandClearUAVFloat)
FRHICOMMAND_MACRO(FRHICommandClearUAVUint)
FRHICOMMAND_MACRO(FRHICommandCopyToResolveTarget)
FRHICOMMAND_MACRO(FRHICommandCopyTexture)
FRHICOMMAND_MACRO(FRHICommandResummarizeHTile)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesDepth)
FRHICOMMAND_MACRO(FRHICommandTransitionTextures)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesArray)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesPipeline)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesArrayPipeline)
FRHICOMMAND_MACRO(FRHICommandClearColorTexture)
FRHICOMMAND_MACRO(FRHICommandClearDepthStencilTexture)
FRHICOMMAND_MACRO(FRHICommandClearColorTextures)
FRHICOMMAND_MACRO(FRHICommandSetGlobalUniformBuffers)
FRHICOMMAND_MACRO(FRHICommandBuildLocalUniformBuffer)
FRHICOMMAND_MACRO(FRHICommandBeginRenderQuery)
FRHICOMMAND_MACRO(FRHICommandEndRenderQuery)
FRHICOMMAND_MACRO(FRHICommandCalibrateTimers)
FRHICOMMAND_MACRO(FRHICommandPollOcclusionQueries)
FRHICOMMAND_MACRO(FRHICommandBeginScene)
FRHICOMMAND_MACRO(FRHICommandEndScene)
FRHICOMMAND_MACRO(FRHICommandBeginFrame)
FRHICOMMAND_MACRO(FRHICommandEndFrame)
FRHICOMMAND_MACRO(FRHICommandBeginDrawingViewport)
FRHICOMMAND_MACRO(FRHICommandEndDrawingViewport)
FRHICOMMAND_MACRO(FRHICommandInvalidateCachedState)
FRHICOMMAND_MACRO(FRHICommandDiscardRenderTargets)
FRHICOMMAND_MACRO(FRHICommandDebugBreak)
FRHICOMMAND_MACRO(FRHICommandUpdateTextureReference)
FRHICOMMAND_MACRO(FRHICommandUpdateRHIResources)
FRHICOMMAND_MACRO(FRHICommandCopyBufferRegion)
FRHICOMMAND_MACRO(FRHICommandCopyBufferRegions)
FRHICOMMAND_MACRO(FRHICommandClearRayTracingBindings)
FRHICOMMAND_MACRO(FRHICommandRayTraceOcclusion)
FRHICOMMAND_MACRO(FRHICommandRayTraceIntersection)
FRHICOMMAND_MACRO(FRHICommandRayTraceDispatch)
FRHICOMMAND_MACRO(FRHICommandSetRayTracingBindings)
FRHICOMMAND_MACRO(FClearCachedRenderingDataCommand)
FRHICOMMAND_MACRO(FClearCachedElementDataCommand)
2.5.4 遊戲執行緒和渲染執行緒的互動
本節將講述各個執行緒之間的資料交換機制和實現細節。首先看看遊戲執行緒如何將資料傳遞給渲染執行緒。
遊戲執行緒在Tick時,會通過UGameEngine、FViewport、UGameViewportClient等物件,才會進入渲染模組的呼叫:
void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
UGameEngine::RedrawViewports()
{
void FViewport::Draw( bool bShouldPresent)
{
void UGameViewportClient::Draw()
{
// 計算ViewFamily、View的各種屬性
ULocalPlayer::CalcSceneView();
// 傳送渲染命令
FRendererModule::BeginRenderingViewFamily()
{
World->SendAllEndOfFrameUpdates();
// 建立場景渲染器
FSceneRenderer* SceneRenderer = FSceneRenderer::CreateSceneRenderer(ViewFamily, ...);
// 向渲染執行緒傳送繪製場景指令.
ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
[SceneRenderer](FRHICommandListImmediate& RHICmdList)
{
RenderViewFamily_RenderThread(RHICmdList, SceneRenderer)
{
(......)
// 呼叫場景渲染器的繪製介面.
SceneRenderer->Render(RHICmdList);
(......)
}
FlushPendingDeleteRHIResources_RenderThread();
});
}
}}}}
前面章節也提到,渲染執行緒使用的是SceneProxy和SceneInfo等物件,那麼遊戲的Actor元件是如何跟場景代理的資料聯絡起來的呢?又是如何更新資料的?
先弄清楚遊戲元件向SceneProxy傳遞資料的機制,答案就藏在FScene::AddPrimitive
:
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp
void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
(......)
// 建立圖元的場景代理
FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
Primitive->SceneProxy = PrimitiveSceneProxy;
if(!PrimitiveSceneProxy)
{
return;
}
// 建立圖元場景代理的場景資訊
FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
(......)
FScene* Scene = this;
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
(......)
SceneProxy->CreateRenderThreadResources();
// 在渲染執行緒中將SceneInfo加入到場景中.
Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
});
}
上面有個關鍵的一句Primitive->CreateSceneProxy()
即是建立元件對應的PrimitiveSceneProxy,在PrimitiveSceneProxy的建構函式中,將元件的所有資料都拷貝了一份:
FPrimitiveSceneProxy::FPrimitiveSceneProxy(const UPrimitiveComponent* InComponent, FName InResourceName)
:
CustomPrimitiveData(InComponent->GetCustomPrimitiveData())
, TranslucencySortPriority(FMath::Clamp(InComponent->TranslucencySortPriority, SHRT_MIN, SHRT_MAX))
, Mobility(InComponent->Mobility)
, LightmapType(InComponent->LightmapType)
, StatId()
, DrawInGame(InComponent->IsVisible())
, DrawInEditor(InComponent->GetVisibleFlag())
, bReceivesDecals(InComponent->bReceivesDecals)
(......)
{
(......)
}
拷貝資料之後,遊戲執行緒修改的是PrimitiveComponent的資料,而渲染執行緒修改或訪問的是PrimitiveSceneProxy的資料,彼此不干擾,避免了臨界區和鎖的同步,也保證了執行緒安全。不過這裡還有疑問,那就是建立PrimitiveSceneProxy的時候會拷貝一份資料,但在建立完之後,PrimitiveComponent是如何向PrimitiveSceneProxy更新資料的呢?
原來是ActorComponent有幾個標記,只要這幾個標記被標記為true
,便會在適當的時機呼叫更新介面,以便得到更新:
// Engine\Source\Runtime\Engine\Classes\Components\ActorComponent.h
class ENGINE_API UActorComponent : public UObject, public IInterface_AssetUserData
{
protected:
// 以下介面分別更新對應的狀態, 子類可以重寫以實現自己的更新邏輯.
virtual void DoDeferredRenderUpdates_Concurrent()
{
(......)
if(bRenderStateDirty)
{
RecreateRenderState_Concurrent();
}
else
{
if(bRenderTransformDirty)
{
SendRenderTransform_Concurrent();
}
if(bRenderDynamicDataDirty)
{
SendRenderDynamicData_Concurrent();
}
}
}
virtual void CreateRenderState_Concurrent(FRegisterComponentContext* Context)
{
bRenderStateCreated = true;
bRenderStateDirty = false;
bRenderTransformDirty = false;
bRenderDynamicDataDirty = false;
}
virtual void SendRenderTransform_Concurrent()
{
bRenderTransformDirty = false;
}
virtual void SendRenderDynamicData_Concurrent()
{
bRenderDynamicDataDirty = false;
}
private:
uint8 bRenderStateDirty:1; // 元件的渲染狀態是否髒的
uint8 bRenderTransformDirty:1; // 元件的變換矩陣是否髒的
uint8 bRenderDynamicDataDirty:1; // 元件的渲染動態資料是否髒的
};
上面protected的介面就是用於重新整理元件的資料到對應的SceneProxy,具體的元件子類可以重寫它,以定製自己的更新邏輯,比如ULightComponent
的變換矩陣更新邏輯如下:
// Engine\Source\Runtime\Engine\Private\Components\LightComponent.cpp
void ULightComponent::SendRenderTransform_Concurrent()
{
// 將變換資訊更新到場景.
GetWorld()->Scene->UpdateLightTransform(this);
Super::SendRenderTransform_Concurrent();
}
而場景的UpdateLightTransform
會將元件的資料組裝起來,並將資料傳送到渲染執行緒執行:
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp
void FScene::UpdateLightTransform(ULightComponent* Light)
{
if(Light->SceneProxy)
{
// 組裝元件的資料到結構體(注意這裡不能將Component的地址傳到渲染執行緒,而是將所有要更新的資料拷貝一份)
FUpdateLightTransformParameters Parameters;
Parameters.LightToWorld = Light->GetComponentTransform().ToMatrixNoScale();
Parameters.Position = Light->GetLightPosition();
FScene* Scene = this;
FLightSceneInfo* LightSceneInfo = Light->SceneProxy->GetLightSceneInfo();
// 將資料傳送到渲染執行緒執行.
ENQUEUE_RENDER_COMMAND(UpdateLightTransform)(
[Scene, LightSceneInfo, Parameters](FRHICommandListImmediate& RHICmdList)
{
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
// 在渲染執行緒執行資料更新.
Scene->UpdateLightTransform_RenderThread(LightSceneInfo, Parameters);
});
}
}
void FScene::UpdateLightTransform_RenderThread(FLightSceneInfo* LightSceneInfo, const FUpdateLightTransformParameters& Parameters)
{
(......)
// 更新變換矩陣.
LightSceneInfo->Proxy->SetTransform(Parameters.LightToWorld, Parameters.Position);
(......)
}
至此,元件如何向場景代理更新資料的邏輯終於理清了。
需要特別提醒的是,FScene、FSceneProxy等有些介面在遊戲執行緒呼叫,而有些介面(一般帶有_RenderThread
的字尾)在渲染執行緒呼叫,切記不能跨執行緒呼叫,否則會產生競爭條件,甚至引發程式崩潰。
2.5.5 遊戲執行緒和渲染執行緒的同步
前面也提到,遊戲執行緒不可能領先於渲染執行緒超過一幀,否則遊戲執行緒會等待渲染執行緒處理完。它們的同步機制涉及兩個關鍵的概念:
// Engine\Source\Runtime\RenderCore\Public\RenderCommandFence.h
// 渲染命令柵欄
class RENDERCORE_API FRenderCommandFence
{
public:
// 向渲染命令佇列增加一個柵欄. bSyncToRHIAndGPU是否同步RHI和GPU交換Buffer, 否則只等待渲染執行緒.
void BeginFence(bool bSyncToRHIAndGPU = false);
// 等待柵欄被執行. bProcessGameThreadTasks沒有作用.
void Wait(bool bProcessGameThreadTasks = false) const;
// 是否完成了柵欄.
bool IsFenceComplete() const;
private:
mutable FGraphEventRef CompletionEvent; // 處理完成同步的事件
ENamedThreads::Type TriggerThreadIndex; // 處理完之後需要觸發的執行緒型別.
};
// Engine\Source\Runtime\Engine\Public\UnrealEngine.h
class FFrameEndSync
{
FRenderCommandFence Fence[2]; // 渲染柵欄對.
int32 EventIndex; // 當前事件索引
public:
// 同步遊戲執行緒和渲染執行緒. bAllowOneFrameThreadLag是否允許渲染執行緒一幀的延遲.
void Sync( bool bAllowOneFrameThreadLag )
{
Fence[EventIndex].BeginFence(true); // 開啟柵欄, 強制同步RHI和GPU交換鏈的.
bool bEmptyGameThreadTasks = !FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::GameThread);
// 保證遊戲執行緒至少跑過一次任務.
if (bEmptyGameThreadTasks)
{
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
}
// 如果允許延遲, 交換事件索引.
if( bAllowOneFrameThreadLag )
{
EventIndex = (EventIndex + 1) % 2;
}
(......)
// 開啟柵欄等待.
Fence[EventIndex].Wait(bEmptyGameThreadTasks);
}
};
而FFrameEndSync
的使用是在FEngineLoop::Tick
中:
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
void FEngineLoop::Tick()
{
(......)
// 在引擎迴圈的幀末尾新增遊戲執行緒和渲染執行緒的同步事件.
{
static FFrameEndSync FrameEndSync; // 區域性靜態變數, 執行緒安全.
static auto CVarAllowOneFrameThreadLag = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.OneFrameThreadLag"));
// 同步遊戲和渲染執行緒, 是否允許一幀的延遲可由控制檯命令控制. 預設是開啟的.
FrameEndSync.Sync( CVarAllowOneFrameThreadLag->GetValueOnGameThread() != 0 );
}
(......)
}
2.6 多執行緒渲染結語
平行計算架構已然成為現代引擎的標配,UE的多執行緒渲染是隨著多核CPU和新一代圖形API誕生而必然的產物。但就目前而言,渲染執行緒很多時候還是單條的(雖然可以藉助TaskGraph部分地並行)。理想情況下,是多條渲染執行緒並行且不依賴地生成渲染命令,並且不需要主執行緒來驅動,任何執行緒都可作為工作執行緒(亦即沒有UE的命名執行緒),任何執行緒都可發起計算任務,避免作業系統級別的功能執行緒。而這需要作業系統、圖形API、計算機語言共同地不斷演化才可達成。
最近釋出的UE4.26已經在普及RDG,RDG可以自動裁剪、優化渲染Pass和資源,是提升引擎整體並行處理的一大利器。
這篇文章原本預計2個月左右完成,然而實際上花了3個多月,幾乎耗盡了筆者的所有業餘時間。原本還有很多技術章節需要新增,但篇幅和時間都超限了,只好作罷。希望此係列文章對學習UE的讀者們有幫助,感謝關注和收藏。
特別說明
- 感謝所有參考文獻的作者,部分圖片來自參考文獻和網路,侵刪。
- 本系列文章為筆者原創,只發表在部落格園上,歡迎分享本文連結,但未經同意,不允許轉載!
- 系列文章,未完待續,完整目錄請戳內容綱目。
- 系列文章,未完待續,完整目錄請戳內容綱目。
- 系列文章,未完待續,完整目錄請戳內容綱目。
參考文獻
- Unreal Engine 4 Sources
- Unreal Engine 4 Documentation
- Rendering and Graphics
- Materials
- Graphics Programming
- Unreal Engine 4 Rendering
- Threaded Rendering
- 《大象無形-虛幻引擎程式設計淺析》
- 《房燕良-虛幻4渲染系統架構解析》
- UE4 Render System Sheet
- Inside UE4
- Exploring in UE4
- 《遊戲引擎架構》
- C++ reference
- 《C++物件導向多執行緒程式設計》
- 《Win32多執行緒程式設計》
- 《Windows核心程式設計(第5版)》
- 《Effective C++》
- 《C++ Concurrency In Action》
- 《Modern Concurrency in Depth》
- 《Multithreaded Programming Guide》
- Windows 95
- 多核處理器
- [Amdahl's law](Amdahl's law https://en.wikipedia.org/wiki/Amdahl's_law)
- Thread-local storage
- Covering Multithreading Basics
- Multi-Threaded Programming: C++11
- 深入理解Coroutine(協程)及其原理以及Coroutine in Kotlin
- C++11 多執行緒
- C++ 的可移植性和跨平臺開發[6]:多執行緒
- volatile (computer programming)
- 談談 C/C++ 中的 volatile
- Practical Parallel Rendering with DirectX 9 and 10
- Multi-engine synchronization
- Understanding DirectX Multithreaded Rendering Performance by Experiments
- Performance, Methods, and Practices of DirectX 11 Multithreaded Rendering
- Introduction to Multithreaded rendering and the usage of Deferred Contexts in DirectX 11
- Practical Parallel Rendering with DirectX 9 and 10
- D3D11和D3D12多執行緒渲染框架的比較
- Render graphs and Vulkan — a deep dive
- DirectX 12: Performance Comparison Between Single- and Multithreaded Rendering when Culling Multiple Lights
- Vulkan - 高效能渲染
- Vulkan 多執行緒渲染
- Vulkan Multi-Threading
- 新一代圖形API VULKAN 對 AR、VR 的影響
- Fork-Join Model
- Fiber (computer science)
- Evaluation of multi-threading in Vulkan
- Metal: Command Organization and Execution Model
- Task-based Multithreading - How to Program for 100 cores
- Multi-Threaded Game Engine Design
- 遊戲引擎隨筆 0x04:平行計算架構
- Multithreaded Rendering & Graphics Jobs 多執行緒渲染與圖形Jobs
- Optimizing Graphics in Unity
- Frostbite Rendering Architecture and Real-time Procedural Shading & Texturing Techniques (GDC 2007)
- FrameGraph: Extensible Rendering Architecture in Frostbite
- Parallelizing the Naughty Dog engine using fibers
- Destiny’s Multi-threaded Renderer Architecture talk
- Multithreading the Entire Destiny Engine
- Everything You Need to Know About Multithreaded Rendering in Fortnite
- bgfx
- 虛幻4 Task Graph System 介紹
- UE4中TaskGraph系統的簡單分析
- Directed acyclic graph
- Scalability for All: Unreal Engine* 4 with Intel
- UE高階效能剖析技術
- UE4 關於主迴圈的資料
- Intel® ISPC User's Guide
- 多執行緒渲染
- UE4裡的渲染執行緒
- UE4主執行緒與渲染執行緒同步
- Bringing Fortnite to Mobile with Vulkan and OpenGL ES