《Exploring in UE4》多线程机制详解[原理分析]

转自:https://zhuanlan.zhihu.com/c_164452593编程

目录
一.概述
二."标准"多线程
三.AsyncTask系统
3.1 FQueuedThreadPool线程池
3.2 Asyntask与IQueuedWork
3.3 其余相关技术细节
四.TaskGraph系统
4.1 从Tick函数谈起
4.2 TaskGraph系统中的任务与线程
4.3 TaskGraph系统中的任务与事件
4.4 其余相关技术细节
五.总结

一.概述

多线程是优化项目性能的重要方式之一,游戏也不例外。虽然常常能看到“游戏不适合利用多线程优化”的言论,但我我的以为这句话更多的是针对GamePlay,游戏中多线程用的一点也很多,好比渲染模块、物理模块、网络通讯、音频系统、IO等。下图就展现了UE4引擎运行时的部分线程,可能比你想象的还要多一些。windows

UE4运行时开启的线程


虽然UE4遵循C++11的标准,可是他并无使用std::thread,而是本身实现了一套多线程机制(应该是从UE3时代就有了,未考证),用法上很像Java。固然,你若是想用std::thread也是彻底没有问题的。

在UE4里面,咱们能够本身继承FRunnable接口建立单个线程,也能够直接建立AsyncTask来调用线程池里面空闲的线程,还能够经过TaskGraph系统来异步完成一些自定义任务。虽然本质相同,可是用法不一样,理解上也要花费很多时间,这篇文章会对里面的各个机制逐个分析并作出总结,但并不会深刻讨论线程的实现原理、线程安全等内容。另外,因为我的接触多线程编程的时间不长,有一些内容可能不是很准确,欢迎你们一块儿讨论。

二.“标准”多线程

咱们先从最基本的建立方式谈起,这里的“标准”只是一个修饰。其实就是建立一个继承自FRunnable的类,把这个类要执行的任务分发给其余线程去执行。FRunnable就是一个很简单的类,里面只有5,6个函数接口,为了与真正的线程区分,我这里称FRunnable为“线程执行体”。数组

//Runnable.h
class CORE_API FRunnable
{
public:
        /**
         * Initializes the runnable object.
         *
         * This method is called in the context of the thread object that aggregates this, not the
         * thread that passes this runnable to a new thread.
         *
         * @return True if initialization was successful, false otherwise
         * @see Run, Stop, Exit
         */
        virtual bool Init()
        {
                return true;
        }

        /**
         * Runs the runnable object.
         *
         * This is where all per object thread work is done. This is only called if the initialization was successful.
         *
         * @return The exit code of the runnable object
         * @see Init, Stop, Exit
         */
        virtual uint32 Run() = 0;

        /**
         * Stops the runnable object.
         *
         * This is called if a thread is requested to terminate early.
         * @see Init, Run, Exit
         */
        virtual void Stop() { }

        /**
         * Exits the runnable object.
         *
         * Called in the context of the aggregating thread to perform any cleanup.
         * @see Init, Run, Stop
         */
        virtual void Exit() { }

        /**
         * Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.
         * If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.
         *
        * @return Pointer to the single thread interface or nullptr if not implemented.
         */
        virtual class FSingleThreadRunnable* GetSingleThreadInterface( )
        {
                return nullptr;
        }

        /** Virtual destructor */
        virtual ~FRunnable() { }
};

看起来这么简单个类,咱们是否是能够不继承他,单独写一个类再把这几个接口放进去呢?固然不行,实际上,在实现多线程的时候,咱们须要将FRunnable做为参数传递到真正的线程里面,而后才能经过线程去调用FRunnable的Run,也就是咱们具体实现的类的Run方法(经过虚函数覆盖父类的Run)。所谓真正的线程其实就是FRunnableThread,不一样平台的线程都继承自他,如FRunnableThreadWin,里面会调用Windows平台的建立线程的API接口。下图给出了FRunnable与线程之间的关系类图:安全

在实现的时候,你须要继承FRunnable并重写他的那几个函数,Run()里面表示你在线程里面想要执行的逻辑。具体的实现方式网上有不少案例,这里给出UE4Wiki的教程连接:

Multi-Threading: How to Create Threads in UE4

三.AsyncTask系统

说完了UE4“标准”线程的使用,下面咱们来谈谈稍微复杂一点的AsyncTask系统。AsyncTask系统是一套基于线程池的异步任务处理系统。若是你没有接触过UE4多线程,用搜索引擎搜索UE4多线程时可能就会看到相似下面这样的用法。服务器

//AsyncWork.h
        class ExampleAsyncTask : public FNonAbandonableTask
        {
                friend class FAsyncTask<ExampleAsyncTask>;

                int32 ExampleData;

                ExampleAsyncTask(int32 InExampleData)
                 : ExampleData(InExampleData)
                {
                }

                void DoWork()
                {
                        ... do the work here
                }

                FORCEINLINE TStatId GetStatId() const
                {
                        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAsyncTask, STATGROUP_ThreadPoolAsyncTasks);
                }
        };

        void Example()
        {

                //start an example job

                FAsyncTask<ExampleAsyncTask>* MyTask = new FAsyncTask<ExampleAsyncTask>( 5 );
                MyTask->StartBackgroundTask();

                //--or --

                MyTask->StartSynchronousTask();

                //to just do it now on this thread
                //Check if the task is done :

                if (MyTask->IsDone())
                {
                }

                //Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.
                //Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.

                MyTask->EnsureCompletion();
                delete Task;
        }

 没错,这就是官方代码里面给出的一种异步处理的解决方案示例。不过你可能更在乎的是这个所谓多线程的用法,看起来很是简单,可是却找不到任何带有“Thread”或“Runnable”的字样,那么他也是用Runnable的方式作的么?答案确定是Yes。只不过封装的比较深,须要咱们深刻源码才能明白其中的原理。网络

注:Andriod多线程开发里面也会用到AsyncTask,两者的实现原理很是类似。多线程

3.1 FQueuedThreadPool线程池

在介绍AsynTask以前先讲一下UE里面的线程池,FQueuedThreadPool。和通常的线程池实现相似,线程池里面维护了多个线程FQueuedThread与多个任务队列IQueuedWork,线程是按照队列的方式来排列的。在引擎PreInit的时候执行相关的初始化操做,代码以下框架

// FEngineLoop.PreInit   LaunchEngineLoop.cpp
if (FPlatformProcess::SupportsMultithreading())
{
        {
                GThreadPool = FQueuedThreadPool::Allocate();
                int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

                // we are only going to give dedicated servers one pool thread
                if (FPlatformProperties::IsServerOnly())
                {
                    NumThreadsInThreadPool = 1;
                }
                verify(GThreadPool->Create(NumThreadsInThreadPool, 128 * 1024));
        }
#ifUSE_NEW_ASYNC_IO
        {
                GIOThreadPool = FQueuedThreadPool::Allocate();
                int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfIOWorkerThreadsToSpawn();
                if (FPlatformProperties::IsServerOnly())
                {
                    NumThreadsInThreadPool = 2;
                }
                verify(GIOThreadPool->Create(NumThreadsInThreadPool, 16 * 1024, TPri_AboveNormal));
        }
#endif// USE_NEW_ASYNC_IO

#ifWITH_EDITOR
        // when we are in the editor we like to do things like build lighting and such
        // this thread pool can be used for those purposes
        GLargeThreadPool = FQueuedThreadPool::Allocate();
        int32 NumThreadsInLargeThreadPool = FMath::Max(FPlatformMisc::NumberOfCoresIncludingHyperthreads() - 2, 2);
                
        verify(GLargeThreadPool->Create(NumThreadsInLargeThreadPool, 128 * 1024));
#endif
}

这段代码咱们能够看出,专有服务器的线程池GThreadPool默认只开一个线程,非专有服务器的根据核数开(CoreNum-1)个线程。编辑器模式会另外再建立一个线程池GLargeThreadPool,包含(LogicalCoreNum-2)个线程,用来处理贴图的压缩和编码相关内容。

在线程池里面全部的线程都是FQueuedThread类型,不过更确切的说FQueuedThread是继承自FRunnable的线程执行体,每一个FQueuedThread里面包含一个FRunnableThread做为内部成员。

相比通常的线程,FQueuedThread里面多了一个成员FEvent* DoWorkEvent,也就是说FQueuedThread里面是有一个事件触发机制的。那么这个事件机制的做用是什么?通常状况下来讲,就是在没有任务的时候挂起这个线程,在添加并分配给该线程任务的时候激活他,不过你能够灵活运用它,在你须要的时候去动态控制线程任务的执行与暂停。前面咱们在给线程池初始化的时候,经过FQueuedThreadPool的Create函数建立了多个FQueuedThread,而后每一个FQueuedThread会执行Run函数,里面有一段逻辑以下:异步

//ThreadingBase.cpp
bool bContinueWaiting = true;
while(bContinueWaiting )
{                                
        DECLARE_SCOPE_CYCLE_COUNTER(TEXT( "FQueuedThread::Run.WaitForWork" ), STAT_FQueuedThread_Run_WaitForWork, STATGROUP_ThreadPoolAsyncTasks );
        // Wait for some work to do
        bContinueWaiting = !DoWorkEvent->Wait( 10 );
}
//windows平台下的wait
bool FEventWin::Wait(uint32 WaitTime, const bool bIgnoreThreadIdleStats/*= false*/)
{
        WaitForStats();

        SCOPE_CYCLE_COUNTER(STAT_EventWait );
        check(Event );

        FThreadIdleStats::FScopeIdleScope(bIgnoreThreadIdleStats );
        return (WaitForSingleObject( Event, WaitTime ) == WAIT_OBJECT_0);
}

咱们看到,当DoWorkEvent执行Wait的时候,若是该线程的Event处于无信号状态(默认刚建立是无信号的),那么wait会等待10毫秒并返回false,线程处于While无限循环中。若是线程池添加了任务(AddQueuedWork)并执行了DoWorkEvent的Trigger函数,那么Event就会被设置为有信号,Wait函数就会返回true,随后线程跳出循环进而处理任务。

编辑器

注:FQueuedThread里的DoWorkEvent是经过FPlatformProcess::GetSynchEventFromPool();从EventPool里面获取的。WaitForSingleObject等内容涉及到Windows下的事件机制,你们能够自行到网上搜索相关的使用,这里给出一个官方的使用案例。


目前咱们接触的类之间的关系以下图:

3.2 Asyntask与IQueuedWork

线程池的任务IQueuedWork自己是一个接口,因此得有具体实现。这里你就应该能猜到,所谓的AsynTask其实就是对IQueuedWork的具体实现。这里AsynTask泛指FAsyncTask与FAutoDeleteAsyncTask两个类,咱们先从FAsyncTask提及。

FAsyncTask有几个特色,

  • FAsyncTask是一个模板类,真正的AsyncTask须要你本身写。经过DoWork提供你要执行的具体任务,而后把你的类做为模板参数传过去
  • 使用FAsyncTask就默认你要使用UE提供的线程池FQueuedThreadPool,前面代码里说明了在引擎PreInit的时候会初始化线程池并返回一个指针GThreadPool。在执行FAsyncTask任务时,若是你在执行StartBackgroundTask的时候会默认使用GThreadPool线程池,固然你也能够在参数里面指定本身建立的线程池
  • 建立FAsyncTask并不必定要使用新的线程,你能够调用函数StartSynchronousTask直接在当前线程上执行任务
  • FAsyncTask自己包含一个DoneEvent,任务执行完成的时候会激活该事件。当你想等待一个任务完成时再作其余操做,就能够调用EnsureCompletion函数,他能够从队列里面取出来还没被执行的任务放到当前线程来作,也能够挂起当前线程等待DoneEvent激活后再往下执行



FAutoDeleteAsyncTask与FAsyncTask是类似的,可是有一些差别,

  • 默认使用UE提供的线程池FQueuedThreadPool,没法使用其余线程池
  • FAutoDeleteAsyncTask在任务完成后会经过线程池的Destroy函数删除自身或者在执行DoWork后删除自身,而FAsyncTask须要手动delete
  • 包含FAsyncTask的特色1和特色3



总的来讲,AsyncTask系统实现的多线程与你本身字节继承FRunnable实现的原理类似,不过他在用法上比较简单,并且还能够直接借用UE4提供的线程池,很方便。

最后咱们再来梳理一下这些类之间的关系:

AsyncTask系统相关类图


3.3 其余相关技术细节

你们在看源码的时候可能会遇到一些疑问,这里简单列举并解释一下

1. FScopeLock

FScopeLock是UE提供的一种基于做用域的锁,思想相似RAII机制。在构造时对当前区域加锁,离开做用域时执行析构并解锁。UE里面有不少带有“Scope”关键字的类,如移动组件中的FScopedMovementUpdate,Task系统中的FScopeCycleCounter,FScopedEvent等,他们的实现思路是相似的。

2. FNonAbandonableTask

继承FNonAbandonableTask的Task不能够在执行阶段终止,即便执行Abandon函数也会去触发DoWork函数。

      // FAutoDeleteAsyncTask
        virtual void Abandon(void)
        {
                if (Task.CanAbandon())
                {
                        Task.Abandon();
                        delete this;
                }
                else
                {
                        DoWork();
                }
        }
        // FAsyncTask
        virtual void Abandon(void)
        {
                if (Task.CanAbandon())
                {
                        Task.Abandon();
                        check(WorkNotFinishedCounter.GetValue() == 1);
                        WorkNotFinishedCounter.Decrement();
                }
                else
                {
                        DoWork();
                }
                FinishThreadedWork();
        }

3.AsyncTask与转发构造

经过本章节开始的例子,咱们知道建立自定义任务的方式以下

FAsyncTask<ExampleAsyncTask>*MyTask= new FAsyncTask<ExampleAsyncTask>(5);

括号里面的5会以参数转发的方式传到的ExampleAsyncTask构造函数里面,这一步涉及到C++11的右值引用与转发构造,具体细节能够去网上搜索一下。

/** Forwarding constructor. */
template <typename Arg0Type, typename... ArgTypes>
FAsyncTask(Arg0Type&& Arg0, ArgTypes&&... Args)
        : Task(Forward<Arg0Type>(Arg0), Forward<ArgTypes>(Args)...)
{
        Init();
}

四.TaskGraph系统

说完了FAsyncTask系统,接下来咱们再谈谈更复杂的TaskGraph系统(应该不会有比他更复杂的了)。Task Graph 系统是UE4一套抽象的异步任务处理系统,能够建立多个多线程任务,指定各个任务之间的依赖关系,按照该关系来依次处理任务。具体的实现方式网上也有不少案例,这里先给出UE4Wiki的教程连接:

Multi-Threading: Task Graph System

建议你们先了解其用法,而后再往下阅读。

4.1 从Tick函数谈起

平时调试的时候,咱们随便找个Tick断点一下都能看到相似下图这样的函数堆栈。若是你前面的章节都看懂的话,这个堆栈也能大概理解。World在执行Tick的时候,触发了FNamedTaskThread线程去执行任务(FTickFunctionTask),任务FTickFunctionTask具体的工做内容就是执行ACtorComponent的Tick函数。其实,这个堆栈也说明了全部Actor与Component的Tick都是经过TaskGraph系统来执行的。

组件Tick的函数堆栈


不过你可能仍是会有不少问题,TaskGraph断点为何是在主线程里面?FNamedTaskThread是什么意思?FTickFunctionTask究竟是在哪一个线程执行?答案在下一小节逐步给出。

4.2 TaskGraph系统中的任务与线程

既然是Task系统,那么应该能猜到他和前面的AsyncTask系统类似,咱们能够建立多个Task任务而后分配给不一样的线程去执行。在TaskGraph系统里面,任务类也是咱们本身建立的,如FTickFunctionTask、FReturnGraphTask等,里面须要声明DoTask函数来表示要执行的任务内容,GetDesiredThread函数来表示要在哪一个线程上面执行,大概的样子以下:

class FMyTestTask
{
        public:
         FMyTestTask()//send in property defaults here
        {
        }
        static const TCHAR*GetTaskName()
        {
                return TEXT("FMyTestTask");
        }
        FORCEINLINE static TStatId GetStatId()
        {
                RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTestTask, STATGROUP_TaskGraphTasks);
        }
        /** return the thread for this task **/
        static ENamedThreads::Type GetDesiredThread()
        {
                return ENamedThreads::AnyThread;
        }

        /*
        namespace ESubsequentsMode
       {
                enum Type
                {
                        // 存在后续任务
                        TrackSubsequents,
                        // 没有后续任务
                        FireAndForget
                };
        }
        */
        static ESubsequentsMode::Type GetSubsequentsMode()
        {
                return ESubsequentsMode::TrackSubsequents;
        }

        void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
        {
                
        }
};

而线程在该系统里面称为FWorkerThread,经过全局的单例类FTaskGraphImplementation来控制建立和分配任务的,默认状况下会开启5个基本线程,额外线程的数量则由下面的函数NumberOfWorkerThreadsToSpawn来决定,FTaskGraphImplementation的初始化在FEngineLoop.PreInit里面进行。固然若是平台自己不支持多线程,那么其余的工做也会在GameThread里面进行。

FTaskGraphImplementation(int32)
{
        bCreatedHiPriorityThreads = !!ENamedThreads::bHasHighPriorityThreads;
        bCreatedBackgroundPriorityThreads = !!ENamedThreads::bHasBackgroundThreads;

        int32 MaxTaskThreads = MAX_THREADS;
        int32 NumTaskThreads = FPlatformMisc::NumberOfWorkerThreadsToSpawn();

        // if we don't want any performance-based threads, then force the task graph to not create any worker threads, and run in game thread
        if (!FPlatformProcess::SupportsMultithreading())
        {
                // this is the logic that used to be spread over a couple of places, that will make the rest of this function disable a worker thread
                // @todo: it could probably be made simpler/clearer
                // this - 1 tells the below code there is no rendering thread
                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;

        // if we don't have enough threads to allow all of the sets asked for, then we can't create what was asked for.
        check(NumTaskThreadSets == 1 || FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS) == NumTaskThreads * NumTaskThreadSets + NumNamedThreads);
        NumThreads = FMath::Max<int32>(FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS), NumNamedThreads + 1);
        .......
}
//GenericPlatformMisc.cpp
int32 FGenericPlatformMisc::NumberOfWorkerThreadsToSpawn()
{
        static int32 MaxGameThreads = 4;
        static int32 MaxThreads = 16;

        int32 NumberOfCores = FPlatformMisc::NumberOfCores();//物理核数,4核8线程的机器返回的是4
        int32 MaxWorkerThreadsWanted = (IsRunningGame() || IsRunningDedicatedServer() || IsRunningClientOnly()) ? MaxGameThreads :MaxThreads;
        // need to spawn at least one worker thread (see FTaskGraphImplementation)
        return FMath::Max(FMath::Min(NumberOfCores - 1, MaxWorkerThreadsWanted), 1);
}

前面提到的FWorkerThread虽然能够理解为工做线程,但其实他不是真正的线程。FWorkerThread里面有两个重要成员,一个是FRunnableThread* RunnableThread,也就是真正的线程。另外一个是FTaskThreadBase* TaskGraphWorker,即继承自FRunnable的线程执行体。FTaskThreadBase有两个子类,FTaskThreadAnyThread和FNamedTaskThread,分别表示非指定名称的任意Task线程执行体和有名字的Task线程执行体。咱们平时说的渲染线程、游戏线程就是有名称的Task线程,而那些咱们建立后尚未使用到的线程就是非指定名称的任意线程。

非指定名称的任意线程


在引擎初始化FTaskGraphImplementation的时候,咱们就会默认构建24个FWorkerThread工做线程(这里支持最大的线程数量也就是24),其中里面有5个是默认带名字的线程,StatThread、RHIThread、AudioThread、GameThread、ActualRenderingThread,还有前面提到的N个非指定名称的任意线程,这个N由CPU核数决定。对于带有名字的线程,他不须要建立新的Runnable线程,由于他们会在其余的时机建立,如StatThread以及RenderingThread会在FEngineLoop.PreInit里建立。而那N个非指定名称的任意线程,则须要在一开始就手动建立Runnable线程,同时设置其优先级比前面线程的优先级要低。到这里,咱们应该能够理解,有名字的线程专门要作他名字对应的事情,非指定名称的任意线程则能够用来处理其余的工做,咱们在CreateTask建立任务时会经过本身写好的函数决定当前任务应该在哪一个线程执行。

运行中全部的WorldThreads


如今咱们能够先回答一下上一节的问题了,FTickFunctionTask究竟是在哪一个线程执行?答案是游戏主线程,咱们能够看到FTickFunctionTask的Desired线程是Context.Thread,而Context.Thread是在下图赋值的,具体细节参考FTickTaskManager与FTickTaskLevel的使用。

/** return the thread for this task **/
FORCEINLINEENamedThreads::TypeGetDesiredThread()
{
        return Context.Thread;
}

context线程类型的初始化


这里咱们再思考一下,若是咱们将多个任务投放到一个线程那么他们是按照什么顺序执行的呢?这个答案须要分两种状况解答,对于投放到FTaskThreadAnyThread执行的任务会在建立的时候按照优先级放到IncomingAnyThreadTasks数组里面,而后每次线程完成任务后会从这个数组里面弹出未执行的任务来执行,他的特色是咱们有权利随时修改和调整这个任务队列。而对于投放到FNamedTaskThread执行的任务,会被放到其自己维护的队列里面,经过FThreadTaskQueue来处理执行顺序,一旦放到这个队列里面,咱们就没法随意调整任务了。

4.3 TaskGraph系统中的任务与事件

虽然前面已经比较细致的描述了TaskGraph系统的框架,可是一个很是重要的特性咱们还没讲到,就是任务依赖的实现原理。怎么理解任务依赖呢?简单来讲,就是一个任务的执行可能依赖于多个事件对象,这些事件对象都触发以后才会执行这个任务。而这个任务完成后,又可能触发其余事件,其余事件再进一步触发其余任务,大概的效果是下图这样。

任务与事件的依赖关系图


每一个任务结束分别触发一个事件,Task4须要等事件A、B都完成才会执行,而且不会接着触发其余事件。Task5须要等事件B、C都完成,而且会触发事件D,D事件不会再触发任何任务。固然,这些任务和事件可能在不一样的线程上执行。

这里再看一下Task任务的建立代码,分析一下先决依赖事件与后续等待事件都是如何产生的。

FGraphEventRef Join=TGraphTask<FVictoryTestTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady();

CreateTask的第一个参数就是该任务依赖事件数组(这里为NULL),若是传入一个事件数组的话,那么当前任务就会经过SetupPrereqs函数设置这些依赖事件,而且在全部依赖事件都触发后再将该任务放到任务队列里面分配给线程执行。

当执行CreateTask时,会经过FGraphEvent::CreateGraphEvent()构建一个新的后续事件,再经过函数ConstructAndDispatchWhenReady返回。这样咱们就能够在当前的位置执行

FTaskGraphInterface::Get().WaitUntilTaskCompletes(Join, ENamedThreads::GameThread_Local);

让当前线程等待该任务结束并触发事件后再继续执行,当前面这个事件完成后,就会调用DispatchSubsequents()去触发他后续的任务。WaitUntilTaskCompletes函数的第二个参数必须是当前的线程类型并且是带名字的。

Task系统相关类图


4.4 其余相关技术细节

1.FThreadSafeCounter

经过调用不一样平台的原子操做来实现线程安全的计数

int32 Add( int32 Amount )
{
        return FPlatformAtomics::InterlockedAdd(&Counter, Amount);
}

2. Task的构造方式

咱们看到相比AsyncTask,TaskGraph的建立可谓是既新奇又复杂,首先要调用静态的CreateTask,而后又要经过返回值执行ConstructAndDispatchWhenReady。那么这么作的目的是什么呢?按照我我的的理解,主要是为了能把想要的参数都传进去。其实每建立一个任务,都须要传入两套参数,一套参数指定依赖事件,属于任务系统的自身特色,另外一套参数传入玩家自定义任务的相关参数。为了实现这个效果,UE先经过工厂方法建立抽象任务把相关特性保存进去,而后经过内部的一个帮助类FConstructor构建一个真正的玩家定义的任务。若是C++玩的不溜,这样的方法还真难想出来。(这是我我的猜想,若是你有更好的理解欢迎留言评论)

3. FScopedEvent

在上一节讲过,带有Scope关键字的基本都是同一个思想,在构造的时候初始化析构的时候执行某些特殊的操做。FScopedEvent做用是在当前做用域内等待触发,若是没有激活该事件,就会一直处于Wait中。

4. WaitUntilTaskCompletes的实现机制

顾名思义,该函数的功能就是在任务结束以前保持当前线程的等待。不过他的实现确实颇有趣,第一个参数是等待的事件Event,第二个参数是当前线程类型。若是当前的线程没有任何Task,他会判断传入的事件数组是否都完成了,完成便可返回,没有完成就会构建一个FReturnGraphTask类型的任务,而后执行ProcessThreadUntilRequestReturn等全部的依赖事件都完成后才会返回。

// named thread process tasks while we wait
TGraphTask<FReturnGraphTask>::CreateTask(&Tasks, CurrentThread).ConstructAndDispatchWhenReady(CurrentThread);
ProcessThreadUntilRequestReturn(CurrentThread);

若是当前的线程有Task任务,他就建立一个ScopeEvent,并执行TriggerEventWhenTasksComplete等待前面传入的Tasks都完成后再返回。

FScopedEvent Event;
TriggerEventWhenTasksComplete(Event.Get(), Tasks, CurrentThreadIfKnown);

五.总结

到这里,咱们已经看到了三种使用多线程的方式,每种机制里面都有不少技术点值得咱们深刻学习。关于机制的选择这里再给出一点建议:

对于消耗大的,复杂的任务不建议使用TaskGraph,由于他会阻塞其余游戏线程的执行。即便你不在那几个有名字的线程上执行,也可能会影响到游戏的其余逻辑。好比物理计算相关的任务就是在非指定名称的线程上执行的。这种复杂的任务,建议你本身继承Runnable建立线程,或者使用AsynTask系统。

而对于简单的任务,或者想比较方便的实现线程的之间的依赖等待关系,直接扔给TaskGraph就能够了。

另外,不要在非GameThread线程内执行下面几个操做:

  • 不要 Spawn / Modify/ delete UObjects or AActors
  • 不要使用定时器 TimerManager
  • 不要使用任何绘制接口,例如 DrawDebugLine



一开始我也不是很理解,因此就在其余线程里面执行了Spawn操做,而后就蹦在了下面的地方。能够看到,SpawnActor的时候会执行物理数据的初始化,而这个操做是必需要在主线程里面执行的,我猜其余的位置确定还有不少相似的宏。至于缘由,我想就是咱们最前面提到的“游戏不适合利用多线程优化”,游戏GamePlay中各个部分很是依赖顺序,多线程没办法很好的处理这些关系。再者,游戏逻辑如此复杂,你怎么作到避免“竞争条件”呢?处处加锁么?我想那样的话,游戏代码就无法看了吧。

在其余线程Spawn致使崩溃


最后,咱们再来一张全家福吧~

多线程系统类图(完整)

相关文章
相关标签/搜索