剖析虛幻渲染體系(02)- 多執行緒渲染

0嚮往0發表於2021-01-25

 

 

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) = \frac{1}{(1-p) + \frac{p}{s}} \]

公式的各個分量含義如下:

  • \(S_{latency}\):整個任務在多執行緒處理中理論上獲得的加速比。
  • \(s\):用於執行任務並行部分的硬體資源的執行緒數量。
  • \(p\):可並行處理的任務佔比。

舉個具體的栗子,假設有8核16執行緒的CPU用於處理某個任務,這個任務有70%的部分是可以並行處理的,那麼它的理論加速比為:

\[S_{latency}(16) = \frac{1}{(1-0.7) + \frac{0.7}{16}} = 2.9 \]

由此可見,多執行緒程式設計帶來的效益並非跟核心數呈直線正比,實際上它的曲線如下所示:

阿姆達爾定律揭示的核心數和加速比圖例。由此可見,可並行的任務佔比越低,加速比獲得的效果越差:當可並行任務佔比為50%時,16核已經基本達到加速比天花板,無論後面增加多少核心數量,都無濟於事;如果可並行任務佔比為95%時,到2048個核心才會達到加速比天花板。

雖然阿姆達爾定律給我們帶來了殘酷的現實,但是,如果我們能夠提升任務並行佔比到接近100%,則加速比天花板可以得到極大提升:

\[S_{latency}(s) = \frac{1}{(1-p) + \frac{p}{s}} = \frac{1}{(1-1) + \frac{1}{s}} = s \]

如上公式所示,當\(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 = *pc = *p只需從記憶體取一次p的值,那麼bc的值必然是10

若考慮volatile的影響,假設執行完b = *p語句之後,p的值被其它執行緒修改了,則執行c = *p會再次從記憶體中讀取p的值,此時c的值不再是10,而是新的值。

但是,volatile並不能解決多執行緒的同步問題,只適合以下三種情況使用:

1、和訊號處理(signal handler)相關的場合。

2、和記憶體對映硬體(memory mapped hardware)相關的場合。

3、和非本地跳轉(setjmplongjmp)相關的場合。

  • 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::promisestd::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::asyncstd::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_awaitco_returnco_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_variablestd::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::Copy B)
釋放A,將B的資料替換到A,但不會影響B的資料。 -
template
void Move(T& A,typename TMoveSupportTraits::Move B)
釋放A,將B的資料替換到A,但會影響B的資料。 -
FNoncopyable 派生它即可實現不可拷貝的物件。 -
TGuardValue 帶作業域的值,可指定一個新值和舊值,作用域內是新值,離開作用域變成舊值。 -
TScopeCounter 帶作用域的計數器,作用域內計數器+1,離開作用域後計數器-1 -
template
typename TRemoveReference::Type&& MoveTemp(T&& Obj)
將引用轉換成右值,可能會修改源值。 std::move
template
T CopyTemp(T& Val)
強制建立右值的拷貝,不會改變源值。 -
template
T&& Forward(typename TRemoveReference::Type& Obj)
將引用轉換成右值引用。 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的讀者們有幫助,感謝關注和收藏。

 

特別說明

  • 感謝所有參考文獻的作者,部分圖片來自參考文獻和網路,侵刪。
  • 本系列文章為筆者原創,只發表在部落格園上,歡迎分享本文連結,但未經同意,不允許轉載
  • 系列文章,未完待續,完整目錄請戳內容綱目
  • 系列文章,未完待續,完整目錄請戳內容綱目
  • 系列文章,未完待續,完整目錄請戳內容綱目

 

參考文獻

相關文章