【UE4 C++ 基礎知識】<12> 多線程——FRunnable


概述

  • UE4里,提供的多線程的方法:
    • 繼承 FRunnable 接口創建單個線程
    • 創建 AsyncTask 調用線程池里面空閑的線程
    • 通過 TaskGraph 系統來異步完成一些自定義任務
    • 支持原生的多線程 std::thread
  • 在GameThread線程之外的其他線程中
    • 不要 spawning / modifying / deleting UObjects / AActors
    • 不要使用定時器 TimerManager
    • 不要使用任何繪制接口,例如 DrawDebugLine,然有可能崩潰
    • 如果想做的話,可以在主線程中異步處理
    • 其他線程中一般做數據收發和解析,數學運算等

本文主要介紹 FRunnable 類

FRunnable

  • FRunnable是UE4中多線程的實現方式之一,適用於復雜運算

  • FRunnable 是線程的執行體,提供相應的接口。FRunnable需要依附與一個FRunnableThread對象,才能被執行

    class CORE_API FRunnable
    {
    public:
        // ....
        virtual bool Init(); // 初始化 runnable 對象,在FRunnableThread創建線程對象后調用
    
        virtual uint32 Run() = 0; // Runnable 對象邏輯處理主體,在Init成功后調用
    
        virtual void Stop() {} // 停止 runnable 對象, 線程提前終止時被用戶調用
    
        virtual void Exit() {} // 退出 runnable 對象,由FRunnableThread調用
    };
    
  • FRunnableThread 表示一個可執行的線程,該類會派生出平台相關的子類。通過調用 FRunnableThread::Create 完成線程的創建

image

快速創建一個線程

創建 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;
	}
}

image

單例線程

  • 當希望線程只能創建一次時,可以通過聲明靜態單例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 > 存儲,移除
  • 也可設定線程結束條件,讓其自行結束線程

線程鎖

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

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM