【UE4 C++ 基础知识】<12> 多线程——FRunnable - 砥才人 - 博客园 (cnblogs.com)
注意事项:
TaskGraph适合有依赖关系的多线程任务。指定使用哪个线程的时候要注意一些逻辑只能在GameThread上调用。如
- 创建、消耗Actor
- Debug绘制函数
- 定时器 TimerManager
UE4里,提供的多线程的方法:
- 继承
FRunnable
接口创建单个线程- 创建
AsyncTask
调用线程池里面空闲的线程- 通过
TaskGraph
系统来异步完成一些自定义任务- 支持原生的多线程
std::thread
FRunnable(线程执行类的基类)
FRunnableThread:线程类,根据平台不同会通过静态方法Create创建不同的线程类,比如FRunnableThreadWin, FRunnableThreadAndroid等等。
使用方法:
- 先派生一个FRunnable的执行类,并创建类对象,
- 然后使用FRunnableThread::Create创建线程并将对象传入。
- 通常做法:创建FRunnable派生类对象并创建线程的过程放在FRunnable派生类的构造函数中。
FRunnable派生类必须重写基类的Run方法。下面是FRunnable类的格式
class CORE_API FRunnable
{
public:
//初始化FRunable对象,该方法在新线程(执行Run的线程)中被执行,而不是在创建FRunable的线程中执行,返回true 表示初始化成功,将执行FRunable的Run方法,否则FRunnableThread中的线程将立即结束,Run不会执行
virtual bool Init() // 初始化 runnable 对象,在FRunnableThread创建线程对象后调用
{
return true;
}
//该方法只有在Init()返回true 时,才执行,是线程中具体执行的任务。
virtual uint32 Run() = 0; // Runnable 对象逻辑处理主体,在Init成功后调用
//用于提供提前停止线程的执行的条件。通常会设置一个停止标志,然后在Run中检测这个标志,检测后停止线程。经常与FRunnableThread的WaitForCompletion方法一起使用。WaitForCompletion方法会阻塞直到线程退出。
//Stop方法在FRunnableThread的kill方法中被调用(kill可以选择等待还是不等待线程结束,kill实际也是通过stop来停止线程,如果stop没有执行停止线程的操作,则skill如果是等待线程,则将一致阻塞),而kill方法也会被FRunnableThread::~FRunnableThread()自动调用,这时kill是等待线程结束的
virtual void Stop() { } // 停止 runnable 对象, 线程提前终止时被用户调用
//该方法在Run结束后执行,该方法只有在Init()返回true 时,才执行,该方法在执行Run的线程中被执行,而不是在创建FRunable的线程中执行
virtual void Exit() { } // 退出 runnable 对象,由FRunnableThread调用
...
};
FRunnable 派生类
// .h
class TIPS_API FSimpleRunnable: public FRunnable
{
public:
FSimpleRunnable(const FString& ThreadName);
~FSimpleRunnable();
void PauseThread(); // 线程挂起 方法一
void WakeUpThread(); // 线程唤醒 方法一
void Suspend(bool bSuspend); // 线程挂起/唤醒 方法二
void StopThread(); // 停止线程,一般用该方法
void ShutDown(bool bShouldWait);// 停止线程,bShouldWait true的时候可强制 kill 线程
private:
FString m_ThreadName;
int32 m_ThreadID;
bool bRun = true; // 线程循环标志
bool bPause = false; //线程挂起标志
FRunnableThread* ThreadIns; // 线程实例
FEvent* ThreadEvent; //FEvent指针,挂起/激活线程, 在各自的线程内使用
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
};
// .cpp
FSimpleRunnable::FSimpleRunnable(const FString& ThreadName)
{
// 获取 FEvent 指针
ThreadEvent = FPlatformProcess::GetSynchEventFromPool();
// 创建线程实例
m_ThreadName = ThreadName;
ThreadIns = FRunnableThread::Create(this, *m_ThreadName, 0, TPri_Normal);
m_ThreadID = ThreadIns->GetThreadID();
UE_LOG(LogTemp, Warning, TEXT("Thread Start! ThreadID = %d"), m_ThreadID);
}
FSimpleRunnable::~FSimpleRunnable()
{
if (ThreadEvent) // 清空 FEvent*
{
FPlatformProcess::ReturnSynchEventToPool(ThreadEvent); // delete ThreadEvent;
ThreadEvent = nullptr;
}
if (ThreadIns) // 清空 FRunnableThread*
{
delete ThreadIns;
ThreadIns = nullptr;
}
}
bool FSimpleRunnable::Init()
{
return true; //若返回 false ,线程创建失败,不会执行后续函数
}
uint32 FSimpleRunnable::Run()
{
int32 count = 0;
FPlatformProcess::Sleep(0.03f); //延时,等待初始化完成
while (bRun)
{
if (bPause)
{
ThreadEvent->Wait(); // 线程挂起
if (!bRun) // 线程挂起时执行线程结束
{
return 0;
}
}
UE_LOG(LogTemp, Warning, TEXT("ThreadID: %d, Count: %d"),m_ThreadID, count);
count++;
FPlatformProcess::Sleep(0.1f); // 执行间隔,防止堵塞
}
return 0;
}
void FSimpleRunnable::Stop()
{
bRun = false;
bPause = false;
if (ThreadEvent)
{
ThreadEvent->Trigger(); // 保证线程不挂起
}
Suspend(false); // 保证线程不挂起,本例只是为了暂时不同的挂起方法,如果不使用Suspend(),无需使用
}
void FSimpleRunnable::Exit()
{
UE_LOG(LogTemp, Warning, TEXT("Thread Exit!"));
}
void FSimpleRunnable::PauseThread()
{
bPause = true;
UE_LOG(LogTemp, Warning, TEXT("Thread Pause!"));
}
void FSimpleRunnable::WakeUpThread()
{
bPause = false;
if (ThreadEvent)
{
ThreadEvent->Trigger(); // 唤醒线程
}
UE_LOG(LogTemp, Warning, TEXT("Thread Wakeup!"));
}
void FSimpleRunnable::Suspend(bool bSuspend)
{
if (ThreadIns)
{
ThreadIns->Suspend(bSuspend); //挂起/唤醒
}
}
void FSimpleRunnable::StopThread()
{
Stop();
ThreadIns->WaitForCompletion(); // 等待线程执行完毕
}
void FSimpleRunnable::ShutDown(bool bShouldWait)
{
if (ThreadIns)
{
ThreadIns->Kill(bShouldWait); // bShouldWait 为false,Suspend(true)时,会崩
}
}
创建调用多线程的Actor
// .h
protected:
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
FSimpleRunnable* SimpleRunnable;
public:
UFUNCTION(BlueprintCallable)
void CreateNewThread(const FString& ThreadName);
UFUNCTION(BlueprintCallable)
void PauseThread();
UFUNCTION(BlueprintCallable)
void SuspendThread(bool bSuspend);
UFUNCTION(BlueprintCallable)
void WakeUpThread();
UFUNCTION(BlueprintCallable)
void StopThread();
UFUNCTION(BlueprintCallable)
void ForceKillThread(bool bShouldWait);
};
// .cpp
void ARunnableActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (SimpleRunnable) // 防止线程挂起,退出无响应
{
SimpleRunnable->StopThread();
delete SimpleRunnable;
SimpleRunnable = nullptr;
}
}
void ARunnableActor::CreateNewThread(const FString& ThreadName)
{
SimpleRunnable = new FSimpleRunnable(ThreadName);
}
void ARunnableActor::PauseThread()
{
if (SimpleRunnable)
{
SimpleRunnable->PauseThread();
}
}
void ARunnableActor::SuspendThread(bool bSuspend)
{
if (SimpleRunnable)
{
SimpleRunnable->Suspend(bSuspend);
}
}
void ARunnableActor::WakeUpThread()
{
if (SimpleRunnable)
{
SimpleRunnable->WakeUpThread();
}
}
void ARunnableActor::StopThread()
{
if (SimpleRunnable)
{
SimpleRunnable->StopThread();
}
}
void ARunnableActor::ForceKillThread(bool bShouldWait)
{
if (SimpleRunnable)
{
SimpleRunnable->ShutDown(bShouldWait);
delete SimpleRunnable;
SimpleRunnable = nullptr;
}
}
单例线程
- 当希望线程只能创建一次时,可以通过声明静态单例FRunnable (本例为FSimpleRunnable)
// .h
static FSimpleRunnable* MySimpleRunnable; // 声明静态单例
static FSimpleRunnable* JoyInit(); // 声明静态方法
// cpp
// 初始化静态单例
FSimpleRunnable* FSimpleRunnable::MySimpleRunnable = nullptr;
//创建 SimpleRunnable 实例
FSimpleRunnable* FSimpleRunnable::JoyInit()
{
if (!MySimpleRunnable && FPlatformProcess::SupportsMultithreading())
{
MySimpleRunnable = new FSimpleRunnable();
}
return MySimpleRunnable;
}
多个线程
当希望执行多个线程时
- 可用TMap<Name, FRunnable > 存储,移除
- 也可设定线程结束条件,让其自行结束线程
使用线程池中的线程
使用线程池通常是通过FAsyncTask和FAutoDeleteAsyncTask来使用的,这两个类都是所谓的异步任务。 异步任务可以用于执行一直运行的异步处理(比如while(1)),也可以用于执行时间较长的异步操作。
使用FAsyncTask需要手动删除,FAutoDeleteAsyncTask会在任务完成时自动删除。
使用异步任务,需要先定义一个异步任务的执行类型,执行类型应该派生自FNonAbandonableTask,具体形式如下
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)//这里的5是传给ExampleAsyncTask构造函数的参数,他的构造函数在当前线程执行;
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;
}
使用TaskGraph系统
Task Graph 系统是UE4一套抽象的异步任务处理系统,可以创建多个多线程任务,指定各个任务之间的依赖关系,按照该关系来依次处理任务。
在引擎初始化FTaskGraphImplementation的时候,我们就会默认构建24个FWorkerThread工作线程(这里支持最大的线程数量也就是24),其中里面有5个是默认带名字的线程,StatThread、RHIThread、AudioThread(声音线程)、GameThread(主线程,游戏线程)、ActualRenderingThread(渲染线程),还有前面提到的N个非指定名称的任意线程,这个N由CPU核数决定。对于带有名字的线程,他不需要创建新的Runnable线程,因为他们会在其他的时机创建,如StatThread以及RenderingThread会在FEngineLoop.PreInit里创建。
taskGraph系统适用于那些比较小的异步处理,大型的异步处理任务,应该使用创建标准线程FRunnable和使用FAsyncTask(尤其不能在GameTread里放置耗时的任务,甚至是while(1),这将严重影响帧率)。
使用TaskGraph,首先要创建TaskGraph所需的任务类型,任务类型创建如下(有点类似FAsyncTask的实际执行类型):
class FGenericTask
{
TSomeType SomeArgument;
public:
FGenericTask(TSomeType InSomeArgument) // CAUTION!: Must not use references in the constructor args; use pointers instead if you need by reference
: 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());
}
};
具体使用如下:
FGraphEventRef Join = TGraphTask<FGenericTask>::CreateTask().ConstructAndDispatchWhenReady(SomeArgument);
//下面的语句是用来检测任务是否完成,不堵塞
if(Join->IsComplete()){....}
//下面的语句将阻塞当前线程等待任务完成
FTaskGraphInterface::Get().WaitUntilTaskCompletes(Join);
Async函数: 多线程任务执行。
TFuture<ResultType> Async(EAsyncExecution Execution, TFunction<ResultType()> Function, TFunction<void()> CompletionCallback = TFunction<void()>())
enum class EAsyncExecution
{
/** Execute in Task Graph (for short running tasks). */
TaskGraph,//这个是使用taskGraph,线程类型为ENamedThreads::AnyThread
/** Execute in separate thread (for long running tasks). */
Thread,//这个是使用做标准的FRunnableThread和FRunnable
/** Execute in global queued thread pool. */
ThreadPool//这个是使用线程池GThreadPool,类似于AsyncTask
};
TFuture,其中ResultType则是传入的执行函数的返回值。
TFunction<int()> My_Task= []() {
return 123;
};
auto Future = Async(EAsyncExecution::TaskGraph, My_Task);
//任务返程后通过下面获得结果
int Result = Future.Get();
AsyncTask函数
其实是使用了taskGraph,可以自己指定线程类型
void AsyncTask(ENamedThreads::Type Thread, TFunction<void()> Function)
{
TGraphTask<FAsyncGraphTask>::CreateTask().ConstructAndDispatchWhenReady(Thread, MoveTemp(Function));
}
线程锁
UE4 线程锁包括:
- FSpinLock 自旋锁
- FScopeLock 区域锁
- FCriticalSection 临界区
- FRWLock 读写锁
本文使用 FScopeLock 、FCriticalSection 作为测试
不使用线程锁
本例使用两个线程 为同一个整数做加法,知道该整数到达目标值
-
修改 SimpleRunnable 代码
FSimpleRunnable(const FString& ThreadName, int32* CurrentNumber, int32 MaxNumber); int32* m_CurrentNumber; int32 m_MaxNumber; int32 m_CalcCount = 0;
FSimpleRunnable::FSimpleRunnable(const FString& ThreadName, int32* CurrentNumber, int32 MaxNumber) { /* 省略部分代码 */ m_CurrentNumber = CurrentNumber; m_MaxNumber = MaxNumber; /* 省略部分代码 */ } uint32 FSimpleRunnable::Run() { FPlatformProcess::Sleep(0.03f); //延时,等待初始化完成 while (bRun && *m_CurrentNumber<m_MaxNumber) { /* 省略部分代码 */ (*m_CurrentNumber)++; m_CalcCount++; if (m_CalcCount % 100 == 0) { UE_LOG(LogTemp, Warning, TEXT("ThreadID: %d, CurrentNumber: %d"),m_ThreadID, *m_CurrentNumber); } FPlatformProcess::Sleep(0.0001f); // 执行间隔,防止堵塞 } return 0; } void FSimpleRunnable::Exit() { UE_LOG(LogTemp, Warning, TEXT("Thread Exit! ThreadID: %d, CurrentNumber: %d, CalcCount: %d"),m_ThreadID, *m_CurrentNumber, m_CalcCount); }
-
修改 RunnableActor 代码
UPROPERTY(EditAnywhere) int32 m_MaxNumber = 1000;
void ARunnableActor::CreateNewThread(const FString& ThreadName) { SimpleRunnable = new FSimpleRunnable(TEXT("Thread1"), &m_CurrentNumber, m_MaxNumber); SimpleRunnable = new FSimpleRunnable(TEXT("Thread2"), &m_CurrentNumber, m_MaxNumber); }
使用线程锁
- 注意 FCriticalSection 是否使用 static 声明
FScopeLock
-
方法一
修改 SimpleRunnable 代码
{ // 注意这个作用域用于 **FScopeLock** static FCriticalSection m_mutex; //声明 staic 可以让线程之间互锁 FScopeLock ScopeLock(&m_mutex); // 该作用域内上锁 (*m_CurrentNumber)++; } m_CalcCount++;
-
方法二
修改 SimpleRunnable 代码
static FCriticalSection m_mutex; //声明 staic 可以让线程之间互锁 FScopeLock* ScopeLock = new FScopeLock(&m_mutex); // 上锁 (*m_CurrentNumber)++; delete ScopeLock; // 解锁
FCriticalSection Lock()/UnLock()
修改 SimpleRunnable 代码
// 放在类声明static ,使用 Lock() 编译不通过
// static 可以让线程之间互锁,不使用 static 锁不生效
// 不使用 static,线程内可以上锁。可以在类中声明
static FCriticalSection m_mutex;
m_mutex.Lock(); // 上锁
(*m_CurrentNumber)++;
m_mutex.Unlock(); // 解锁