• 剖析虚幻渲染体系(02)- 多线程渲染


    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...
    ";
        std::thread t(foo); // 构造线程对象,且传入被执行的函数。
     
        std::cout << "waiting for thread to finish..." << std::endl;
        t.join(); // 加入主线程,使得主线程必须等待该线程执行完毕。
     
        std::cout << "done!
    ";
    }
    

    输出:

    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() << '
    ';
        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 << '
    ';
        }
        void bar(const std::string& str) {
            std::lock_guard<std::mutex> lk(m);
            std::cout << str << '
    ';
        }
        int operator()(int i) {
            std::lock_guard<std::mutex> lk(m);
            std::cout << i << '
    ';
            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()) << '
    ';
     
        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() << '
    '; // 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 << '
    ';
        }
    }
    

    输出:

    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
    ";
        data += " after processing";
     
        // 发送数据给主线程
        processed = true;
        std::cout << "Worker thread signals data processing completed
    ";
     
        // 手动解锁, 以便主线程获得锁.
        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
    ";
        }
        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 << '
    ';
     
        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!
    Results are: " << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '
    ';
        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。它提供的功能有加载、存储、赋值等操作,在底层实现上,会采用平台相关的原子操作接口实现:

    // EngineSourceRuntimeCorePublicTemplatesAtomic.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的声明:

    // EngineSourceRuntimeCorePublicAsyncFuture.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的一个应用案例:

    // EngineSourceRuntimeEnginePrivateComponentsActorComponent.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是所有可以在多个线程并行地运行的物体的父类,它提供的基础接口如下:

    // EngineSourceRuntimeCorePublicHALRunnable.h
    
    class CORE_API FRunnable
    {
    public:
        virtual bool Init();    // 初始化, 成功返回True.
        virtual uint32 Run();    // 运行, 只有Init成功才会被调用.
        virtual void Stop();    // 请求提前停止.
        virtual void Exit();    // 退出, 清理数据.
    };
    

    FRunnable及其子类是可运行于多线程的对象,而与之对立的是只在单线程运行的类FSingleThreadRunnable

    // EngineSourceRuntimeCorePublicMiscSingleThreadRunnable.h
    
    // 多线程禁用下的单线程运行的物体
    class CORE_API FSingleThreadRunnable
    {
    public:
        virtual void Tick();
    };
    

    FRunnable的子类非常多,以下是常见的部分核心子类及其解析。

    • FRenderingThread:运行于渲染线程上的对象。后面有章节会专门剖析。

    • FRHIThread:运行于RHI线程上的对象。后面有章节会专门剖析。

    • FRenderingThreadTickHeartbeat:运行于心跳渲染线程上的物体。

    • FTaskThreadBase:在线程执行的任务父类,后面会有章节专门解析这部分。

    • FQueuedThread:可存储在线程池的线程父类。提供的接口如下:

      // EngineSourceRuntimeCorePrivateHALThreadingBase.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:异步地在单独线程运行的任务,是个模板类,声明如下:

      // EngineSourceRuntimeCorePublicAsyncAsync.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是可运行线程的父类,提供了一组用于管理线程生命周期的接口。它提供的基础接口和解析如下:

    // EngineSourceRuntimeCorePublicHALRunnableThread.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平台的线程实现。它的接口和实现如下:

      // EngineSourceRuntimeCorePrivateWindowsWindowsRunnableThread.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的声明:

    // EngineSourceRuntimeCorePublicHALThreadManager.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的接口如下:

    // EngineSourceRuntimeCorePublicMiscIQueuedWork.h
    
    class IQueuedWork
    {
    public:
        virtual void DoThreadedWork() = 0; // 执行队列化的任务.
        virtual void Abandon() = 0; // 提前放弃执行, 并通知队列里的所有对象清理数据.
    };
    

    由于IQueuedWork只是抽象类,并没有实际执行代码,故而主要子类TAsyncQueuedWork承担了实现代码的任务,以下是TAsyncQueuedWork的声明和实现:

    // EngineSourceRuntimeCorePublicAsyncAsync.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的声明:

    // EngineSourceRuntimeCorePublicMiscQueuedThreadPool.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中,如下:

    // EngineSourceRuntimeCorePrivateHALThreadingBase.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 引擎预初始化已经有提及:

    // EngineSourceRuntimeLaunchPrivateLaunchEngineLoop.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为我们做事,则使用示例如下:

    // EngineSourceRuntimeEnginePrivateShadowMap.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的任务,是个基础父类,其派生的具体任务子类才会执行任务。它的声明(节选)如下:

    // EngineSourceRuntimeCorePublicAsyncTaskGraphInterfaces.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声明如下:

    // EngineSourceRuntimeCorePublicAsyncAsync.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的接口,部分接口和实现如下:

    // EngineSourceRuntimeCorePrivateAsyncTaskGraph.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系统中:

    // EngineSourceRuntimeLaunchPrivateLaunchEngineLoop.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声明了全部对外的接口,部分如下:

    // EngineSourceRuntimeRenderCorePublicRenderingThread.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承载了渲染线程的主要工作,它的部分接口和实现代码如下:

    // EngineSourceRuntimeRenderCorePrivateRenderingThread.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实例,它的实现代码如下(节选):

    // EngineSourceRuntimeRenderCorePrivateRenderingThread.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中,实现代码如下:

    // EngineSourceRuntimeRenderCorePrivateRenderingThread.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中:

    // EngineSourceRuntimeRHIPublicRHICommandList.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线程的相关实现机制跟渲染线程类型,且更加简洁。以下是它的使用示范:

    // EngineSourceRuntimeRHIPublicRHICommandList.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);
    };
    
    // EngineSourceRuntimeRHIPublicRHICommandListCommandExecutes.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

    // EngineSourceRuntimeRendererPrivateRendererScene.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,便会在适当的时机调用更新接口,以便得到更新:

    // EngineSourceRuntimeEngineClassesComponentsActorComponent.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的变换矩阵更新逻辑如下:

    // EngineSourceRuntimeEnginePrivateComponentsLightComponent.cpp
    
    void ULightComponent::SendRenderTransform_Concurrent()
    {
        // 将变换信息更新到场景.
        GetWorld()->Scene->UpdateLightTransform(this);
        Super::SendRenderTransform_Concurrent();
    }
    

    而场景的UpdateLightTransform会将组件的数据组装起来,并将数据发送到渲染线程执行:

    // EngineSourceRuntimeRendererPrivateRendererScene.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 游戏线程和渲染线程的同步

    前面也提到,游戏线程不可能领先于渲染线程超过一帧,否则游戏线程会等待渲染线程处理完。它们的同步机制涉及两个关键的概念:

    // EngineSourceRuntimeRenderCorePublicRenderCommandFence.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; // 处理完之后需要触发的线程类型.
    };
    
    // EngineSourceRuntimeEnginePublicUnrealEngine.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中:

    // EngineSourceRuntimeLaunchPrivateLaunchEngineLoop.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的读者们有帮助,感谢关注和收藏。

    特别说明

    • 感谢所有参考文献的作者,部分图片来自参考文献和网络,侵删。
    • 本系列文章为笔者原创,只发表在博客园上,欢迎分享本文链接,但未经同意,不允许转载
    • 系列文章,未完待续,完整目录请戳内容纲目
    • 系列文章,未完待续,完整目录请戳内容纲目
    • 系列文章,未完待续,完整目录请戳内容纲目

    参考文献

  • 相关阅读:
    iOS 面试题汇总
    iOS管理文件和目录
    文件管理
    Cocoapods依赖管理
    IOS开发几何类方法 CGGeometry.h文件
    CGContext
    respondsToSelector的相关使用
    Java数组5作业(2015-8-27)
    Java错误1(2015-8-27)
    Java数组4(2015-8-27)
  • 原文地址:https://www.cnblogs.com/timlly/p/14327537.html
Copyright © 2020-2023  润新知