文/Jerish 专栏: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引擎运行时的部分线程,可能比你想象的还要多一些。

093735i3ay0fdrivdbwaia.jpgUE4运行时开启的线程
虽然UE4遵循C++11的标准,但是他并没有使用std::thread,而是自己实现了一套多线程机制(应该是从UE3时代就有了,未考证),用法上很像Java。当然,你如果想用std::thread也是完全没有问题的。

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

二.“标准”多线程

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

  1. //Runnable.h
  2. class CORE_API FRunnable
  3. {
  4. public:
  5. /**
  6. * Initializes the runnable object.
  7. *
  8. * This method is called in the context of the thread object that aggregates this, not the
  9. * thread that passes this runnable to a new thread.
  10. *
  11. * @return True if initialization was successful, false otherwise
  12. * @see Run, Stop, Exit
  13. */
  14. virtual bool Init()
  15. {
  16. return true;
  17. }
  18.  
  19. /**
  20. * Runs the runnable object.
  21. *
  22. * This is where all per object thread work is done. This is only called if the initialization was successful.
  23. *
  24. * @return The exit code of the runnable object
  25. * @see Init, Stop, Exit
  26. */
  27. virtual uint32 Run() = 0;
  28.  
  29. /**
  30. * Stops the runnable object.
  31. *
  32. * This is called if a thread is requested to terminate early.
  33. * @see Init, Run, Exit
  34. */
  35. virtual void Stop() { }
  36.  
  37. /**
  38. * Exits the runnable object.
  39. *
  40. * Called in the context of the aggregating thread to perform any cleanup.
  41. * @see Init, Run, Stop
  42. */
  43. virtual void Exit() { }
  44.  
  45. /**
  46. * Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.
  47. * If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.
  48. *
  49. * @return Pointer to the single thread interface or nullptr if not implemented.
  50. */
  51. virtual class FSingleThreadRunnable* GetSingleThreadInterface( )
  52. {
  53. return nullptr;
  54. }
  55.  
  56. /** Virtual destructor */
  57. virtual ~FRunnable() { }
  58. };


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

Multi-Threading: How to Create Threads in UE4

三.AsyncTask系统

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

 

 

  1. //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;
                                                                      • }

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提供的线程池,很方便。

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

            093736o3qti2yi323j4i1e.jpgAsyncTask系统相关类图
            3.3 其他相关技术细节

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

            1. FScopeLock

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

            2. FNonAbandonableTask

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

             
            1. // 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();
                                                                  • }
            组件Tick的函数堆栈
            不过你可能还是会有很多问题,TaskGraph断点为什么是在主线程里面?FNamedTaskThread是什么意思?FTickFunctionTask到底是在哪个线程执行?答案在下一小节逐步给出。

            4.2 TaskGraph系统中的任务与线程

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

             
            1. 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)
                                                                                    • {
                                                                                      •  
                                                                                      • }
                                                                                        • };
            非指定名称的任意线程
            在引擎初始化FTaskGraphImplementation的时候,我们就会默认构建24个FWorkerThread工作线程(这里支持最大的线程数量也就是24),其中里面有5个是默认带名字的线程,StatThread、RHIThread、AudioThread、GameThread、ActualRenderingThread,还有前面提到的N个非指定名称的任意线程,这个N由CPU核数决定。对于带有名字的线程,他不需要创建新的Runnable线程,因为他们会在其他的时机创建,如StatThread以及RenderingThread会在FEngineLoop.PreInit里创建。而那N个非指定名称的任意线程,则需要在一开始就手动创建Runnable线程,同时设置其优先级比前面线程的优先级要低。到这里,我们应该可以理解,有名字的线程专门要做他名字对应的事情,非指定名称的任意线程则可以用来处理其他的工作,我们在CreateTask创建任务时会通过自己写好的函数决定当前任务应该在哪个线程执行。

            093737fr0zous5c4cx41c4.jpg运行中所有的WorldThreads
            现在我们可以先回答一下上一节的问题了,FTickFunctionTask到底是在哪个线程执行?答案是游戏主线程,我们可以看到FTickFunctionTask的Desired线程是Context.Thread,而Context.Thread是在下图赋值的,具体细节参考FTickTaskManager与FTickTaskLevel的使用。

             
            1. /** return the thread for this task **/
              • FORCEINLINEENamedThreads::TypeGetDesiredThread()
                • {
                  • return Context.Thread;
                    • }
            context线程类型的初始化
            这里我们再思考一下,如果我们将多个任务投放到一个线程那么他们是按照什么顺序执行的呢?这个答案需要分两种情况解答,对于投放到FTaskThreadAnyThread执行的任务会在创建的时候按照优先级放到IncomingAnyThreadTasks数组里面,然后每次线程完成任务后会从这个数组里面弹出未执行的任务来执行,他的特点是我们有权利随时修改和调整这个任务队列。而对于投放到FNamedTaskThread执行的任务,会被放到其本身维护的队列里面,通过FThreadTaskQueue来处理执行顺序,一旦放到这个队列里面,我们就无法随意调整任务了。

            093738rowja0t4oooomcta.jpg
            4.3 TaskGraph系统中的任务与事件

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

            093738thm5qe8e8vit4fll.jpg任务与事件的依赖关系图
            每个任务结束分别触发一个事件,Task4需要等事件A、B都完成才会执行,并且不会接着触发其他事件。Task5需要等事件B、C都完成,并且会触发事件D,D事件不会再触发任何任务。当然,这些任务和事件可能在不同的线程上执行。

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

             
            1. FGraphEventRef Join=TGraphTask<FVictoryTestTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady();
            Task系统相关类图
            4.4 其他相关技术细节

            1.FThreadSafeCounter

            通过调用不同平台的原子操作来实现线程安全的计数

             
            1. int32 Add( int32 Amount )
              • {
                • return FPlatformAtomics::InterlockedAdd(Counter, Amount);
                  • }
            在其他线程Spawn导致崩溃
            最后,我们再来一张全家福吧~

            093739yywylna4a6nakliw.jpg多线程系统类图(完整)

            锐亚教育

            锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛