UE 多线程补充

UE4 多线程 创建与使用_ue4多线程-CSDN博客

【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);
    }
    

    image

使用线程锁

  • 注意 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; // 解锁
    

    image

FCriticalSection Lock()/UnLock()

修改 SimpleRunnable 代码

// 放在类声明static ,使用 Lock() 编译不通过
// static 可以让线程之间互锁,不使用 static 锁不生效
// 不使用 static,线程内可以上锁。可以在类中声明
static FCriticalSection m_mutex;

m_mutex.Lock();  // 上锁

(*m_CurrentNumber)++;	

m_mutex.Unlock(); // 解锁

image

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值