你的浏览器不支持canvas

让我们携手共创奇迹!

Thread

Date: Author: LBD

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自WindCrazy

一些关于UE的Thread的知识

注意

What Not to Do不该做什么

  • 不要尝试在其他线程修改、创建或删除 UObject!

您可以准备所有数据/执行所有计算,但只有游戏线程应该实际生成/修改/删除 UObjects/AActors。

  • Dont try to use TimerManager outside of the game thread 😃不要尝试在游戏线程之外使用 TimerManager 😃
  • Don’t try to draw debug lines/points etc, as it will likely crash, ie DrawDebugLine(etc…)不要尝试绘制调试线/点等,因为它可能会崩溃,即 DrawDebugLine(etc…)

Notice (since 4.11):注意(自4.11起):

If you want to use the timer, remove, and modify variables use it:如果您想使用计时器、删除和修改变量,请使用它:

#include “Async.h” … AsyncTask(ENamedThreads::GameThread, { // code to execute on game thread here });#include“异步.h” … AsyncTask(ENamedThreads::GameThread, { // 在游戏线程上执行的代码 });

遇到的问题

另一个线程使用引用捕获失败

问题描述:UE中使用runnable创建出来的线程里使用AsyncTask(ENamedThreads::Type::GameThread,[Location,Rotation,this](){}可以捕获Location和Rotation但是如果使用引用捕获则不行,这是为什么

//当前线程:Runnable的线程
FVector Location = FVector(lon,lat,alt);
FRotator Rotation = FRotator(pitch,heading,roll);
AsyncTask(ENamedThreads::Type::GameThread,[/*&*/Location,/*&*/Rotation,this]()
{
    UE_LOG(LogTemp,Warning,TEXT("%f"),Location.X);
    if (!OnReceiveSyncTrans.ExecuteIfBound(Location, Rotation))
    {
       UE_LOG(LogTemp,Warning,TEXT("OnReceiveSyncTrans not execute"));
    }
});
使用引用捕获Location和Rotation可能会导致问题。因为在创建Lambda表达式时,Location和Rotation可能已经超出了它们的作用域,在Lambda执行时可能找不到这些变量的有效引用。这就是为什么引用捕获Location和Rotation可能会导致问题的原因。 超出作用域后Location和Rotation的生命周期结束,导致无效,而this关键字默认是以引用的方式捕获的,因此在Lambda中可以访问this指针所指向的对象的成员变量和成员函数。

悬垂引用:若以引用隐式或显式捕获非引用实体,而在该实体的生存期结束之后调用lambda对象的函数调用运算符,则发生未定义行为。C++ 的闭包并不延长被捕获的引用的生存期。这同样适用于被捕获的this指针所指向的对象的生存期。因此,在使用this捕获的情况下,需要确保Lambda的执行发生在this指向的对象的有效生命周期内。

获取当前线程ID和名字

每个线程都有一个线程ID,线程ID在它所属的进程环境中有效。为增加标识性,UE4还增加了线程名称。线程ID是唯一的,线程名称可以重复。可通过GetThreadIDGetThreadName 获取线程ID和名称。

uint32 CurrentThreadId = FPlatformTLS::GetCurrentThreadId();
FString CurrentThreadName = FThreadManager::Get().GetThreadName(CurrentThreadId);

FRunnable的使用

参考:UE4多线程FRunnable

FRunnable是UE4中多线程的实现方式之一,适用于复杂运算。

FRunnable需要依附与一个FRunnableThread对象,才能被执行。

  1. FRunnableThread通过FRunnableThread::Create来调用对应平台的接口来创建线程,并调用 FRunnable 对象的Init,Run等事件。Create执行之后,对应的线程函数Run立即执行,并没有Start之类的方法。
  2. FRunnable定义了Init(),Run(),Exit()等虚函数,首先执行Init,初始化完成后执行Run,Run执行完毕之后执行Exit。 主动调用Stop,可以提前终止线程。
  3. 如果要结束线程,可以调用FRunnableThread的Kill方法。Kill方法仅仅调用了RunnableObject的Stop方法(既然你已经拿到了RunnableObject,就直接调用Stop方法即可),因此需要手动在Stop方法中将退出信息传递给Run函数,从而结束Run方法。
class FMyRunnable : public FRunnable
{
protected:
    // FRunnable interface
    virtual bool Init() override;// 初始化 runnable 对象,在FRunnableThread创建线程对象后调用
    virtual uint32 Run() override;// Runnable 对象逻辑处理主体,在Init成功后调用
    virtual void Stop() override;// 停止 runnable 对象, 线程提前终止时被用户调用 如:Kill()或直接调用Stop()
    virtual void Exit() override;// 退出 runnable 对象,由FRunnableThread调用,在Run函数return后会调用此函数。
    // ~FRunnable interface

private:
	FRunnableThread* Thread;
	FThreadSafeBool bRunning;
}
//D:\UE_5.4\Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.cpp 用Windows平台举例
uint32 FRunnableThreadWin::Run()
{
    uint32 ExitCode = 1;
    check(Runnable);

    if (Runnable->Init() == true)	//初始化 runnable 对象,在FRunnableThread创建线程对象后调用
    {
       ThreadInitSyncEvent->Trigger();

       SetTls();

       ExitCode = Runnable->Run();	// Runnable 对象逻辑处理主体,在Init成功后调用

       Runnable->Exit();			// 退出 runnable 对象,由FRunnableThread调用

#if STATS
       FThreadStats::Shutdown();
#endif
       FreeTls();
    }
    else
    {
       ThreadInitSyncEvent->Trigger();
    }

    return ExitCode;
}
//D:\UE_5.4\Engine\Source\Runtime\Core\Private\Microsoft\MicrosoftRunnableThread.h
// Kill函数大致如下,会先调用Runnable的Stop函数,而且应当注意,如果Suspend(true)时调用Kill(false)会崩
virtual bool Kill(bool bShouldWait) override
{
   // Let the runnable have a chance to stop without brute force killing
   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

      WaitForSingleObject(Thread, INFINITE);
   }

   CloseHandle(Thread);
   Thread = NULL;

   return bDidExitOK;
}

获取线程名字

FRunnable有自己的获取名字和ID的方法需要通过FRunnableThread调用

Thread->GetThreadName();

当然通过FThreadManager仍然是可以的,因为通过FRunnableThread 创建的线程是通过FThreadManager进行统一管理。

FThreadManager::Get().AddThread(ThisThread->GetThreadID(), ThisThread);

UE多线程源码剖析

参考:引擎架构剖析——UE4多线程使用和分析(三)

GraphTask

GraphTask是Unreal Engine 4中的一个任务调度系统,用于并行执行任务和管理任务之间的依赖关系,Graph Task可以将任务分配给多个线程并行执行,以充分利用多核处理器的计算能力,并且可以管理任务之间的依赖关系,确保任务按照正确的顺序执行。依赖关系主要通过设置Prerequisites来实现。GraphTask可以很方便的实现异步和基于优先级执行。是一种对任务很方便管理的类。

img

依赖关系的实现并不复杂,如上主要是计算Prerequisites列表所以得Graph队列是否全部执行完毕,全部执行完再运行执行该Graph。通过使用TGraphTask,开发者可以将游戏中的复杂计算和任务分解为多个。

FTaskGraphImplementation

FTaskGraphImplementation是真正为TGraphTask服务和管理的类,负责创建和管理任务图,其中包含了多个任务节点和它们之间的依赖关系。它会根据任务之间的依赖关系和优先级来确定任务的执行顺序,并将任务分配给可用的线程进行并行执行。它会根据系统的硬件配置和任务的负载情况来动态调整线程的数量,以充分利用多核处理器的计算能力。

FAsyncTask

FAsyncTask是Unreal Engine 4中的一个异步任务管理类,可以将耗时的操作设置为异步执行,可以设置任务完成后的回调函数,可以取消正在执行的任务可以为任务设置优先级,以确保重要的任务优先执行。FAsyncTask适用于需要在后台执行的独立任务,如文件加载、网络请求、复杂计算等。它可以在任务执行完毕后进行相应的处理,如更新游戏状态、刷新UI等。而TGraphTask适用于需要并行处理的任务集合,处理依赖关系保证顺序等等,如复杂的计算、渲染,动画,物理数据更新等。

FRunnable

FRunnable是UE更底层的控制线程机制的类,提供了一些方法来控制任务的执行,如启动线程、暂停线程、恢复线程和停止线程等,也提供了一些线程同步的机制,如互斥锁(Mutex)和信号量(Semaphore),用于保护共享资源的访问,避免多线程竞争导致的数据不一致问题。FRunnable定义了线程的生命周期,包括线程的创建、启动、执行和结束等。开发者可以根据需要实现相应的方法来管理线程的生命周期。总之这是一个更底层的控制线程的类方法。

FRunnableThread 可以创建和管理后台线程,用于执行FRunnable中定义的任务。FRunnableThread是一个线程管理类,用于创建和管理后台线程。它提供了一种方便的方式来启动、暂停、恢复和停止线程。而FRunnable是一个接口,用于定义可运行的线程类。它定义了线程的执行逻辑和任务,并提供了线程同步和生命周期管理的功能。开发者可以通过继承FRunnable接口来实现自定义的可运行线程类,并在FRunnableThread中创建和管理该线程。FRunnableThread会调用FRunnable的方法来执行任务,并提供线程同步和生命周期管理的支持。简单说来就是一个是线程管理器,一个是线程类本身。

img

FQueuedThreadPool

FQueuedThreadPool是UE4的线程池,用于管理和调度多个线程执行任务。线程池内有大量的QueueWork来管理线程,包括待执行线程,正在执行的线程和所有线程。在UE中有IO线程池GIOThreadPool ,后台线程池GBackgroundPriorityThreadPool ,以及游戏线程池GThreadPool。线程池主要是给FAsyncTask使用的,也就是这类的异步Task全部是通过线程池来执行任务的。

ParallelForImpl

ParallelForImpl并行计算用于实现并行循环执行任务的功能。它可以将一个任务分割成多个子任务,并在多个线程上并行执行这些子任务,以提高执行效率和性能。提供了一种方便的方式来控制循环的迭代次数和索引范围。ParallelFor会自动处理线程同步和任务分配的细节,确保子任务在不同线程上正确地执行,并在所有子任务完成后进行合并。这样可以避免多线程竞争导致的数据不一致问题。。它通过任务分割和线程同步来实现任务的并行执行,并根据系统的硬件配置和任务的负载情况进行性能优化。我们可以使用ParallelForImpl来自定义并行循环执行任务的逻辑,以满足特定的需求和优化要求。

img

FTaskGraphImplementation

FTaskGraphImplementation是真正为TGraphTask服务和管理的类,负责创建和管理任务图,其中包含了多个任务节点和它们之间的依赖关系。它会根据任务之间的依赖关系和优先级来确定任务的执行顺序,并将任务分配给可用的线程进行并行执行。它会根据系统的硬件配置和任务的负载情况来动态调整线程的数量,以充分利用多核处理器的计算能力。

img

几大线程相关类关系

如上图,展示了几个线程相关类的相互关系。在FTaskGraphImplementation中还管理了游戏主要线程的创建和销毁,主要工作线程包括WorkerThreads工作线程和ENamedThreads。所有的线程数据都存储在FWorkerThread WorkerThreads[MAX_THREADS];内部。前N个是NamedThread,后边是WorkerThread.

多线程教程链接:【UE·引擎篇】Runnable、TaskGraph、AsyncTask、Async多线程开发指南

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

UE4/UE5的TaskGraph

b站:【合集】UE4 C++进阶(进行中)

2w字 + 40张图带你参透并发编程!

参考链接

1,Multi-Threading: How to Create Threads in UE4

2,UE 多线程

3,UE4多线程FRunnable


对于本文内容有问题或建议的小伙伴,欢迎提出问题