概要
UnrealEngine5 の任意のスレッドで実行できる FRunnable を使ってみたメモ書きです。
更新履歴
日付 | 内容 |
---|---|
2024/12/06 | 初版 |
参考
以下の記事を参考にいたしました、ありがとうございます。
UE公式:FRunnable
UE5 Multithreading With FRunnable And Thread Workflow
MultiThreading in C++ with UE5
Multithreading and Performance in Unreal
環境
Windows10
Visual Studio 2022
UnrealEngine 5.3.2
関連ソース
"Engine\Source\Runtime\Core\Public\HAL\Runnable.h"
"Engine\Source\Runtime\Core\Public\HAL\RunnableThread.h"
FRunnableクラスについて
クラスの説明を読むと以下のように書かれています。
FRunnableオブジェクトは、任意のスレッドで「実行」されるオブジェクトです。呼び出しの使用パターンは、Init()、Run()、Exit() です。このオブジェクトを「実行」するスレッドは、常にこれらの呼び出しセマンティクスを使用します。これは、スレッド固有の使用 (TLS など) がこれらの呼び出しのコンテキストで使用できるように、作成されたスレッドで実行されます。「実行可能」は、Init() ですべての初期化を実行します。
初期化が失敗すると、スレッドは実行を停止し、エラー コードを返します。成功した場合は、実際のスレッド作業が行われる場所で Run() が呼び出されます。完了すると、適切なクリーンアップを可能にするために Exit() が呼び出されます。
実行テスト
ワーカークラス
スレッド競合を防ぐため動作スイッチ変数を用意して FAtomic型にしています。
#pragma once
#include "CoreMinimal.h"
#include "HAL/Runnable.h"
class SAMPLE_API FMyWorker : public FRunnable
{
public:
FMyWorker();
virtual ~FMyWorker() override;
bool Init() override;
uint32 Run() override;
void Stop() override;
// メインスイッチとして機能するブール値
TAtomic<bool> bInputReady = false;
// 入出力を行う変数
int32 ExampleIntInput = 0;
float ExampleFloatOutput = 0.0f;
private:
// スレッドハンドル
FRunnableThread* Thread;
// スレッドを終了するタイミングを知るために使用され、Stop() で変更され、Run() で読み取られます。
TAtomic<bool> bRunThread;
};
bInputReady が true のときのみワーカー側が変数の側読み書きができ、false時は外部の別スレッドから読み書きができるようにしています。
#include "MyRunnable.h"
#include "HAL/ThreadingBase.h"
FMyWorker::FMyWorker()
{
// スレッドオブジェクトの構築
Thread = FRunnableThread::Create(this, TEXT("MyTestThread"));
}
FMyWorker::~FMyWorker()
{
if (Thread)
{
// スレッドを削除(ブロック呼び出し)
Thread->Kill();
delete Thread;
}
}
// 初期化(データの割り当てなどを行う)
bool FMyWorker::Init()
{
// スレッドを中止したい場合はfalseを返す
return true;
}
// 実行処理
uint32 FMyWorker::Run()
{
// ここでプロセッサを集中的に使用するタスクを実行します。
// この例では、Stop が呼び出されたときにのみ終了する、終了しないタスクが作成されます。
while (bRunThread)
{
if (bInputReady.Load())
{
// ここで集中的なタスクを実行します。
// すべての入力変数を安全に変更できます。
// 何か負荷の高い処理
ExampleFloatOutput = ExampleIntInput*10;
FPlatformProcess::Sleep(1.0f);
// ここまで
// 外部から読み取り不可なスイッチを落とす
bInputReady = false;
// スレッドを少しスリープさせる
FPlatformProcess::Sleep(0.01f);
}
}
return 0;
}
// 停止
void FMyWorker::Stop()
{
bRunThread.Store(false);
}
実行
作成したワーカークラスのインスタンスを生成し実行する例。
実行1回目は変数の書き込みをして、読みこんだ変数はまだ処理されていません。
2回目で1回目に書き込んだ変数から出力が得られます。
// FMyWorker* _Woker = nullptr; が定義されている
// ワーカーを作成
if(_Worker == nullptr){
_Worker = new FMyWorker();
}
...省略...
// 実行
if(_Worker){
// 入出力が可能か?
if (!_Worker->bInputReady.Load())
{
// 変数の書き込み
_Worker->ExampleIntInput = 12345;
// 変数の読み込み
float _WorkerVal = _Worker->ExampleFloatOutput;
UE_LOG(LogTemp, Warning, TEXT("%f"), _WorkerVal)
// スイッチを切り替えてワーカー スレッドを実行させる
_Worker->bInputReady = true;
}
}
...省略...
// ワーカーを始末
if(_Worker){
_Worker->Stop();
delete _Worker;
_Worker = nullptr;
}
まとめ
競合を起こす可能性のある変数をFAtomic型やクリティカルセクションを使用することで安全に読み書きができるようになりますが、結局ここの待ち時間が大きくなるとスレッドで処理している意味がなくなりますし、設計が難しいです。