ue-async-threading について
バックグラウンド処理、並列計算、同期方法を選ぶ UE 5.8 開発者向けスキルです。UE::Tasks、Async、スレッドプール、専用スレッド、タイマーと UObject のスレッド安全規則を扱います。
主な機能
- 01UE::Tasks とタスク依存関係
- 02Async、TFuture、ゲームスレッドへのディスパッチ
- 03スレッドプールタスクと ParallelFor
- 04専用 FRunnable スレッド
- 05同期機構、Ticker、Timer
活用シーン
- 01計算処理をゲームスレッド外へ移動
- 02メッシュやデータ群の並列処理
- 03共有リソースを使うタスクの順序制御
- 04フレームごとまたは遅延コールバックの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ quodsoler/ unreal-engine-skills --skill ue-async-threadingインストール後、Claude Code で/ue-async-threadingを入力して読み込まれたか確認します。
同梱ファイルreferences/thread-safety-guide.mdreferences/threading-patterns.md
詳細
UE 非同期処理とスレッド処理
対象エンジン:UE 5.8。以下の API は 5.8 のヘッダーで検証済みです。古い形式は「非推奨 — 使用しないでください」に記載しています。
このスキルでは、ゲームスレッド外で処理を行い、結果を戻す方法を扱います。対象は UE::Tasks システム(Tasks/Task.h、Tasks/Pipe.h)、Async/TFuture(Async/Async.h、Async/Future.h)、スレッドプールの FAsyncTask(Async/AsyncWork.h)、ParallelFor、専用 FRunnable スレッド、ロックとロックフリーキュー、FTSTicker、FTimerManager です。タイマー以外はすべて Core モジュールにあり、FTimerManager は Engine に、ENQUEUE_RENDER_COMMAND は RenderCore にあります。Build.cs:PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });。レンダーコマンドをキューに追加する場合に限り、"RenderCore" も追加してください。
コンテキスト
.agents/ue-project-context.md が存在する場合は読み込んでください(モジュール名、規約、有効なプラグイン、GAS/ネットワーク設定)。ファイルがない場合も処理を続けてください。
依頼内容とコードベースから該当する領域を特定してください。異なるコードになる可能性のある解釈が複数ある場合に限り、質問してください。
| 依頼内容 | 参照先 |
|---|---|
| 適切な API の選択 | パターンの選択 |
| 単発のバックグラウンド処理、チェーン化、前提条件、イベント | UE::Tasks |
| 専用スレッドを使わずにリソースへのアクセスを直列化する | FPipe と FTaskConcurrencyLimiter |
| フューチャー、プロミス、ゲームスレッドへのディスパッチ | Async、TFuture、AsyncTask |
| 再利用可能なプール型処理単位 | FAsyncTask と FAutoDeleteAsyncTask |
| データ並列ループ | ParallelFor |
| 長時間稼働する専用スレッド | FRunnable と FRunnableThread |
| ロック、イベント、アトミック、キュー、共有ポインター | 同期処理 |
| フレームごとのコールバックと遅延呼び出し | ティッカーとタイマー |
| UObject、GC、レンダースレッドのルール | スレッドセーフティのルール |
| 古い形式 | 非推奨 — 使用しないでください |
スレッドモデル
| スレッド | 確認方法 | 所有するもの |
|---|---|---|
| ゲームスレッド | IsInGameThread()(CoreGlobals.h) | すべての UObject アクセス、Blueprint、ゲームプレイ、タイマー、ティッカー |
| レンダースレッド | IsInRenderingThread() | シーンプロキシ、レンダーコマンド(ENQUEUE_RENDER_COMMAND) |
| RHI スレッド | IsInRHIThread() | GPU コマンドの送信 |
| ワーカースレッド | なし | UE::Tasks スケジューラー、TaskGraph の AnyThread、ParallelFor |
| スレッドプール | なし | GThreadPool、GBackgroundPriorityThreadPool、GIOThreadPool。GLargeThreadPool は WITH_EDITOR の場合のみ(Misc/QueuedThreadPool.h) |
基本原則:UObject はゲームスレッド専用です。プレーンデータを使ってスレッド外で計算し、スレッドセーフティのルールを参照して、TWeakObjectPtr 経由でゲームスレッド上に結果を適用してください。
パターンの選択
| 要件 | 使用するもの | 結果 |
|---|---|---|
| 単発のバックグラウンド処理、依存関係、チェーン化 | UE::Tasks::Launch | TTask<T> |
| 任意の場所からゲームスレッドでラムダを実行する | AsyncTask(ENamedThreads::GameThread, ...) | なし |
| 実行コンテキストを選択できるフューチャー形式の結果 | Async(EAsyncExecution, ...) | TFuture<T> |
| 1 つのリソースに触れるタスクを直列化する(FIFO) | UE::Tasks::FPipe | TTask<T> |
| 同時に実行するタスク数を制限する | UE::Tasks::FTaskConcurrencyLimiter | なし |
| 所有者がライフタイムを管理する再利用可能なプール型処理単位 | FAsyncTask<T>/FAutoDeleteAsyncTask<T> | GetTask() 経由 |
| 呼び出し元がブロックするデータ並列ループ | ParallelFor | なし |
| 長時間稼働するスレッド(ソケット、ファイル監視、シミュレーションループ) | FRunnable と FRunnableThread::Create | 手動 |
| Actor の Tick 外でのフレームごとのコールバック | FTSTicker::GetCoreTicker().AddTicker | ハンドル |
| ゲームスレッド上での遅延呼び出しまたは繰り返し呼び出し | FTimerManager::SetTimer | FTimerHandle |
UE::Tasks
#include "Tasks/Task.h"
// Tasks/Task.h:299
template<typename TaskBodyType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);
// Tasks/Task.h:324 — same, with a prerequisites collection as the third parameter
template<typename TaskBodyType, typename PrerequisitesCollectionType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
PrerequisitesCollectionType&& Prerequisites,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);
using namespace UE::Tasks;
TArray<int32> Data;
TTask<int32> Sum = Launch(UE_SOURCE_LOCATION, [Data]() { return ComputeSum(Data); });
// Prerequisites: TaskB runs after TaskA completes. Handles are copyable; GetResult() is non-const.
TTask<FVector> TaskA = Launch(UE_SOURCE_LOCATION, []() { return FVector(1.f, 2.f, 3.f); });
TTask<void> TaskB = Launch(UE_SOURCE_LOCATION,
[TaskA]() mutable { const FVector Pos = TaskA.GetResult(); ConsumePosition(Pos); },
Prerequisites(TaskA), ETaskPriority::BackgroundNormal);
// Manual gate: nothing after the event starts until Trigger()
FTaskEvent Gate{ UE_SOURCE_LOCATION };
TTask<void> Gated = Launch(UE_SOURCE_LOCATION, []() { DoWork(); }, Prerequisites(Gate));
Gate.Trigger();
// Wait for a group, with timeout (Tasks/Task.h:393)
TArray<FTask> Group{ TaskB, Gated };
const bool bAllDone = Wait(Group, FTimespan::FromMilliseconds(5.0));
// Cooperative cancellation (Tasks/Task.h: FCancellationToken)
FCancellationToken Token;
Launch(UE_SOURCE_LOCATION, [&Token]() { for (int32 i = 0; i < 1000; ++i) { if (Token.IsCanceled()) { return; } Step(i); } });
Token.Cancel();
// Already-completed task holding a value (useful for uniform interfaces)
TTask<int32> Ready = MakeCompletedTask<int32>(42);
ハンドル API(Tasks/Task.h:36-95):IsValid()、IsCompleted()、Wait()、Wait(FTimespan Timeout) は bool を返します。TryRetractAndExecute() はタスクが開始されていない場合にインラインで実行し、GetResult() は待機した後に ResultType& を返します(check(IsValid()))。フリー関数:Wait(FTask&)、Wait(Collection, FTimespan)、インデックスを返す WaitAny(Collection, Timeout)、入力のいずれかが完了すると完了する FTask を返す Any(Collection)、実行中のタスク内から呼び出す AddNested(Task)(ネストしたタスクが完了するまで親タスクは完了しません)。
優先度(Async/Fundamental/Task.h:20、Tasks/TaskPrivate.h:59-93):ETaskPriority::High | Normal (= Default) | BackgroundHigh | BackgroundNormal | BackgroundLow | Inherit。EExtendedTaskPriority::None | Inline | TaskEvent | GameThreadNormalPri | GameThreadHiPri | GameThreadNormalPriLocalQueue | GameThreadHiPriLocalQueue | RenderThreadNormalPri | RenderThreadHiPri | RHIThreadNormalPri | RHIThreadHiPri(および LocalQueue のバリエーション)。ETaskFlags::None | DoNotRunInsideBusyWait。EExtendedTaskPriority::GameThreadNormalPri を渡すと、ボディはゲームスレッドで実行されます。
レガシーの TaskGraph FGraphEventRef は Launch の前提条件引数に直接渡せます(Tasks/TaskPrivate.h:266)。また、それらのコレクションは WaitAny/Any で使用できます(Tasks/Task.h:417, 468)。そのため、UE::Tasks で TaskGraph の処理を待機できます。チェーン全体の例:threading-patterns.md。
FPipe and FTaskConcurrencyLimiter
FPipe はタスクを 1 つずつ実行するため(追加の前提条件がない場合は FIFO)、リソースを保護する専用スレッドの代わりになります。パイプは最後のタスクより長く存続する必要があります。~FPipe() は !HasWork() をアサートします。
#include "Tasks/Pipe.h"
#include "Tasks/TaskConcurrencyLimiter.h"
UE::Tasks::FPipe SavePipe{ TEXT("SavePipe") }; // explicit FPipe(const TCHAR* InDebugName)
UE::Tasks::TTask<void> First = SavePipe.Launch(UE_SOURCE_LOCATION, []() { WriteChunk(0); });
UE::Tasks::TTask<bool> Second = SavePipe.Launch(UE_SOURCE_LOCATION, []() { return WriteChunk(1); },
UE::Tasks::ETaskPriority::BackgroundNormal);
const bool bInsidePipe = SavePipe.IsInContext(); // true only while a pipe task runs on this thread
SavePipe.WaitUntilEmpty(); // before destroying the pipe
UE::Tasks::FTaskConcurrencyLimiter Limiter(4 /*MaxConcurrency*/, UE::Tasks::ETaskPriority::BackgroundHigh);
for (int32 Index = 0; Index < 64; ++Index)
{
Limiter.Push(UE_SOURCE_LOCATION, [Index](uint32 Slot) { ProcessWithScratch(Index, Slot); }); // Slot in [0, MaxConcurrency)
}
Limiter.Wait(); // Wait(FTimespan Timeout = FTimespan::MaxValue())
FPipe::Launch(const TCHAR*, TaskBody, [Prerequisites,] ETaskPriority = Default, EExtendedTaskPriority = None, ETaskFlags = None)(Tasks/Pipe.h:63,90)。FTaskConcurrencyLimiter は、タスクの完了前に破棄できます。その Wait は一度満たされると、再び有効にはなりません(Tasks/TaskConcurrencyLimiter.h:171-212)。
Async, TFuture and AsyncTask
#include "Async/Async.h"
// Async/Async.h:299
template<typename CallableType>
auto Async(EAsyncExecution Execution, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr) -> TFuture<decltype(Forward<CallableType>(Callable)())>;
// Async/Async.h:407 — takes a reference, so pass *GThreadPool
template<typename CallableType>
auto AsyncPool(FQueuedThreadPool& ThreadPool, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr, EQueuedWorkPriority InQueuedWorkPriority = EQueuedWorkPriority::Normal);
// Async/Async.h:430
template<typename CallableType>
auto AsyncThread(CallableType&& Callable, uint32 StackSize = 0, EThreadPriority ThreadPri = TPri_Normal, TUniqueFunction<void()> CompletionCallback = nullptr);
// Async/Async.h:463
CORE_API void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function);
EAsyncExecution(Async/Async.h:27) | 実行場所 |
|---|---|
TaskGraph | ワーカースレッド。短時間のタスク向け |
TaskGraphMainThread | ゲームスレッド。GC または PostLoad の待機中に実行される場合があります。どこでも安全なコードにのみ使用してください |
TaskGraphMainTick | Tick 中のゲームスレッド。デリゲートや UObject のコードに適した安全な選択肢 |
Thread | 新しい専用スレッド。長時間実行する処理やブロッキング I/O 向け |
ThreadIfForkSafe | Thread と同じですが、フォークに対応しています |
ThreadPool | GThreadPool |
LargeThreadPool | GLargeThreadPool。WITH_EDITOR の場合のみ |
TFuture<FMyResult> Future = Async(EAsyncExecution::ThreadPool, []() { return ComputeResult(); });
if (Future.IsReady()) { UseResult(Future.Get()); } // non-blocking check
FMyResult Copy = Future.Get(); // blocks; does NOT invalidate (Async/Future.h:226)
Future.Next([](FMyResult Value) { UseResult(Value); }); // continuation receives the value; runs on the completing thread
TPromise<FMyResult> Promise;
TFuture<FMyResult> FromPromise = Promise.GetFuture(); // call once
Async(EAsyncExecution::Thread, [P = MoveTemp(Promise)]() mutable { P.SetValue(ComputeResult()); });
AMyActor* MyActor = FindMyActor();
AsyncTask(ENamedThreads::GameThread, [WeakActor = TWeakObjectPtr<AMyActor>(MyActor), Copy]()
{
if (AMyActor* Actor = WeakActor.Get()) { Actor->ApplyResult(Copy); }
});
TFuture<T>(Async/Future.h:210-440):Get()、IsReady()、IsValid()、Wait()、WaitFor(const FTimespan&)、WaitUntil(const FDateTime&)、TFuture<T> を受け取る Then(Func)、T を受け取る Next(Func)(どちらも状態を移動し、この Future を無効にします。Async/Future.h:669)、値を移動して無効にする Consume()、TSharedFuture<T> を返す Share()、Reset()。TPromise<T>(:527):GetFuture()、SetValue(const T&)、SetValue(T&&)、EmplaceValue(Args&&...)。継続処理は Promise を完了させたスレッドで実行されます。ゲームスレッドに切り替えるには、明示的に切り替えてください。
FAsyncTask と FAutoDeleteAsyncTask
FQueuedThreadPool 上で再利用できる作業単位です。FNonAbandonableTask(Async/AsyncWork.h:666)をサブクラス化し、DoWork() と GetStatId() を実装します。FAsyncTask<T> は自身のコンストラクター内で T を構築するため、friend 宣言を使えば、T のコンストラクターを private にできます(エンジンの AsyncWork.h:26-42 にある例でも同様です)。コードが Task->GetTask() を通じて読み取るメンバーは、アクセスが friend の外部で行われるため、public でなければなりません。
#include "Async/AsyncWork.h"
class FMyChunkTask : public FNonAbandonableTask
{
public:
friend class FAsyncTask<FMyChunkTask>;
friend class FAutoDeleteAsyncTask<FMyChunkTask>;
explicit FMyChunkTask(TArray<int32> InInput) : Input(MoveTemp(InInput)) {}
int32 Result = 0;
void DoWork()
{
for (int32 Value : Input) { Result += Value; }
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyChunkTask, STATGROUP_ThreadPoolAsyncTasks);
}
private:
TArray<int32> Input;
};
// Owner-managed lifetime
TArray<int32> Numbers;
FAsyncTask<FMyChunkTask>* Task = new FAsyncTask<FMyChunkTask>(MoveTemp(Numbers));
Task->StartBackgroundTask(); // (FQueuedThreadPool* = GThreadPool, EQueuedWorkPriority = Normal, EQueuedWorkFlags = None, int64 RequiredMemory = -1, const TCHAR* DebugName = nullptr)
const bool bReady = Task->IsDone(); // poll once per frame, never spin
Task->EnsureCompletion(); // (bool bDoWorkOnThisThreadIfNotStarted = true, bool bIsLatencySensitive = false)
const int32 Sum = Task->GetTask().Result;
delete Task;
// Fire-and-forget: deletes itself after DoWork
(new FAutoDeleteAsyncTask<FMyChunkTask>(MoveTemp(Numbers)))->StartBackgroundTask();
その他のメンバー(Async/AsyncWork.h:415-558):StartSynchronousTask(...) はインラインで実行します。Cancel() は、まだキューに入っていた場合に true を返します。WaitCompletionWithTimeout(float TimeLimitSeconds)。IsWorkDone() は負荷の軽い、ブロックしない確認方法です。優先度の低い作業には、プールとして GBackgroundPriorityThreadPool を渡します。テンプレート全体については、threading-patterns.md を参照してください。
ParallelFor
#include "Async/ParallelFor.h"
// Async/ParallelFor.h:526, :543
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
inline void ParallelFor(const TCHAR* DebugName, int32 Num, int32 MinBatchSize, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
// Async/ParallelFor.h:792 — Body is called as Body(ContextType&, int32 Index)
template <typename ContextType, typename ContextAllocatorType, typename FunctionType>
inline void ParallelForWithTaskContext(const TCHAR* DebugName, TArray<ContextType, ContextAllocatorType>& OutContexts, int32 Num, int32 MinBatchSize, const FunctionType& Body, EParallelForFlags Flags = EParallelForFlags::None);
TArray<UStaticMesh*> Meshes;
ParallelFor(Meshes.Num(), [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); });
ParallelFor(TEXT("ProcessMeshes"), Meshes.Num(), 64, [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); },
EParallelForFlags::Unbalanced | EParallelForFlags::BackgroundPriority);
struct FMyScratch { TArray<FVector> Buffer; };
TArray<FMyScratch> Contexts; // one per worker task, reused across iterations
ParallelForWithTaskContext(TEXT("Normals"), Contexts, Meshes.Num(), 32,
[&Meshes](FMyScratch& Scratch, int32 Index) { Scratch.Buffer.Reset(); ComputeNormals(Meshes[Index], Scratch.Buffer); });
EParallelForFlags(Async/ParallelFor.h:46) | 効果 |
|---|---|
None | デフォルト |
ForceSingleThread | 呼び出し元で逐次実行します(デバッグ用) |
Unbalanced | 各イテレーションのコストに大きな差がある場合、小さなバッチに分割します |
PumpRenderingThread | 待機中に呼び出し元がレンダーコマンドを処理します |
BackgroundPriority | ワーカーをバックグラウンド優先度で実行します |
以下も利用できます:ParallelForTemplate(...)(TFunctionRef による間接呼び出しなし、:496)、ParallelForWithPreWork(...)(:571、処理に参加する前に呼び出し元側の処理を実行)、ParallelForWithTaskContext(OutContexts, Num, ContextConstructor, Body, Flags)(:722)、ParallelForWithExistingTaskContext(TArrayView<ContextType> Contexts, Num, MinBatchSize, Body, Flags)(:815)。呼び出し元も処理に参加し、すべてのイテレーションが完了するまでブロックします。CVar Async.ParallelFor.DisableOversubscription(GParallelForDisableOversubscription、Async/ParallelFor.h:43)を使うと、ParallelFor が追加のワーカーを起動しないようにできます。
FRunnable と FRunnableThread
専用の長時間実行スレッドにのみ使用してください。新しいスレッドでのライフサイクルは、Init() → Run() → Exit() です。Stop() は Kill() によって外部から呼び出されます。シグナルを送る処理のみを行う必要があります。
// HAL/Runnable.h:32-69 — override verbatim
virtual bool Init();
virtual uint32 Run() = 0;
virtual void Stop();
virtual void Exit();
virtual class FSingleThreadRunnable* GetSingleThreadInterface(); // return a fallback for -nothreading platforms, or nullptr
// HAL/RunnableThread.h:44
static CORE_API FRunnableThread* Create(
class FRunnable* InRunnable,
const TCHAR* ThreadName,
uint32 InStackSize = 0,
EThreadPriority InThreadPri = TPri_Normal,
uint64 InThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask(),
EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None);
virtual bool Kill(bool bShouldWait = true) = 0; // calls Stop(); with bShouldWait blocks until Run() returns
virtual void WaitForCompletion() = 0;
virtual void Suspend(bool bShouldPause = true) = 0;
virtual void SetThreadPriority(EThreadPriority NewPriority) = 0;
EThreadPriority(GenericPlatform/GenericPlatformAffinity.h:25):TPri_Normal, TPri_AboveNormal, TPri_BelowNormal, TPri_Highest, TPri_Lowest, TPri_SlightlyBelowNormal, TPri_TimeCritical。必ず Kill(true) を呼び出してから FRunnableThread* を delete してください。待機せずに終了させると、メモリリークやデッドロックが発生する可能性があります(ヘッダーコメント HAL/RunnableThread.h:77-79)。スピン待機する代わりに、FPlatformProcess::Sleep(float Seconds) でスリープするか、FEventRef でブロックしてください。シャットダウン処理を含むテンプレート全体については、threading-patterns.md を参照してください。
同期
| 必要な用途 | 型 | RAII ガード | ヘッダー |
|---|---|---|---|
| 汎用ミューテックス、再帰可能 | FCriticalSection (= UE::FPlatformRecursiveMutex) | FScopeLock Lock(&Mutex)。スコープ内で解放するには FScopeUnlock | HAL/CriticalSection.h:53、Misc/ScopeLock.h:140 |
| 複数リーダー、単一ライター、再帰不可 | FRWLock (= UE::FPlatformRWLock) | FReadScopeLock(FRWLock&)、FWriteScopeLock(FRWLock&)、FRWScopeLock(Lock, SLT_ReadOnly / SLT_Write) | HAL/CriticalSection.h:56、Misc/ScopeRWLock.h:92-198 |
| 1 バイト、再帰不可、公平性なし、最速 | UE::FMutex | UE::TUniqueLock<UE::FMutex>。UE::DeferLock を使う UE::TDynamicUniqueLock | Async/Mutex.h:18、Async/UniqueLock.h:19,48、Async/LockTags.h:12 |
| 小型の再帰可能ミューテックス | UE::FRecursiveMutex | UE::TUniqueLock | Async/RecursiveMutex.h:19 |
| 小型のリーダー/ライター用 | UE::FSharedMutex (LockShared/UnlockShared) | UE::TSharedLock、UE::TUniqueLock | Async/SharedMutex.h:22、Async/SharedLock.h:21 |
Lock()/Unlock() を持つ任意の型をガード | UE::TScopeLock<MutexType> | — | Misc/ScopeLock.h:25 |
TArray<FVector> Points;
mutable FCriticalSection Mutex; // mutable so const getters can lock
void Add(const FVector& P) { FScopeLock Lock(&Mutex); Points.Add(P); }
TMap<FName, FVector> Cache;
mutable FRWLock CacheLock;
FVector Read(FName Key) const { FReadScopeLock Lock(CacheLock); return Cache.FindRef(Key); }
void Write(FName Key, FVector V) { FWriteScopeLock Lock(CacheLock); Cache.Add(Key, V); }
int32 Counter = 0;
UE::FMutex SmallMutex;
void Bump() { UE::TUniqueLock Lock(SmallMutex); ++Counter; }
イベント: FEventRef Event(EEventMode::AutoReset) (HAL/Event.h:129-139) は、RAII によるプール管理の FEvent です。Event->Trigger()、Event->Wait()、Event->Wait(uint32 WaitTimeMs)、Event->Reset() を使用します。生のプール操作:FEvent* E = FPlatformProcess::GetSynchEventFromPool(bool bIsManualReset = false) / FPlatformProcess::ReturnSynchEventToPool(E) (GenericPlatformProcess.h:786,799)。UE::FManualResetEvent (Async/ManualResetEvent.h):Notify()、Wait()、WaitFor(FMonotonicTimeSpan)、Reset()。タスク間では UE::Tasks::FTaskEvent を優先してください。これを待機してもワーカーはブロックされません。
アトミック: std::atomic<T> (<atomic>。Templates/Atomic.h によりすでにインクルードされています)。FThreadSafeCounter、FThreadSafeBool、TAtomic は各ヘッダーで非推奨とされています(非推奨 — 使用しないでくださいを参照)。純粋なフラグやカウンターには std::memory_order_relaxed を使用し、アトミック変数を介して他のデータを公開する場合は acquire/release を使用します。
キュー: TMpscQueue<T> (Containers/MpscQueue.h) と TSpscQueue<T> (Containers/SpscQueue.h):Enqueue(Args&&...)、bool Dequeue(T& Out)、TOptional<T> Dequeue()、T* Peek()、IsEmpty()。コンシューマーは 1 つだけです。TQueue<T, EQueueMode> は引き続きコンパイルできますが、「非推奨化を予定」とされています (Containers/Queue.h:11)。
共有ポインター: TSharedPtr、TSharedRef、TWeakPtr、MakeShared はデフォルトで ESPMode::ThreadSafe を使用します (Templates/SharedPointerFwd.h:24-27、SharedPointer.h:2110)。参照カウントはアトミックですが、指し先は保護されません。ホットなシングルスレッド処理に限り、ESPMode::NotThreadSafe を選択してください。
ティッカーとタイマー
どちらもゲームスレッドで実行されます。FTSTicker はエンジン全体で利用され、レベル変更後も存続します。FTimerManager は UWorld ごとに存在し、そのワールドの一時停止に従います。
#include "Containers/Ticker.h"
// Inside AMyActor, which declares: void Poll(float DeltaTime); void OnFire();
// Containers/Ticker.h:45,56,66 — delegate returns true to keep ticking, false to remove itself
FTSTicker::FDelegateHandle TickHandle = FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateWeakLambda(this, [this](float DeltaTime) { Poll(DeltaTime); return true; }), 0.0f /*InDelay*/);
FTSTicker::FDelegateHandle Named = FTSTicker::GetCoreTicker().AddTicker(TEXT("MyPoll"), 0.5f, [](float DeltaTime) { return true; });
FTSTicker::RemoveTicker(TickHandle); // static; safe with an expired handle
FTickerDelegate は DECLARE_DELEGATE_RetVal_OneParam(bool, FTickerDelegate, float) (Containers/Ticker.h:21) です。自身を登録するオブジェクトには FTSTickerObjectBase をサブクラス化し、virtual bool Tick(float DeltaTime) = 0 をオーバーライドします(:136-158)。
#include "TimerManager.h"
// Engine/Public/TimerManager.h:167-237, 247-268, 281-291
FTimerHandle FireHandle, OnceHandle, DelegateHandle; // normally UPROPERTY-free members of AMyActor
FTimerManager& Timers = GetWorldTimerManager(); // AActor; elsewhere GetWorld()->GetTimerManager()
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 1.0f, /*InbLoop*/ true, /*InFirstDelay*/ -1.f);
Timers.SetTimer(OnceHandle, FTimerDelegate::CreateWeakLambda(this, [this]() { OnFire(); }), 2.0f, false);
Timers.SetTimer(DelegateHandle, FTimerDelegate::CreateUObject(this, &AMyActor::OnFire), 1.0f, false, 0.25f);
FTimerManagerTimerParameters Params; // TimerManager.h:124
Params.bLoop = true; Params.bMaxOncePerFrame = true; Params.FirstDelay = 0.5f;
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 0.1f, Params);
FTimerHandle NextTick = Timers.SetTimerForNextTick(this, &AMyActor::OnFire);
Timers.PauseTimer(FireHandle); Timers.UnPauseTimer(FireHandle);
const float Remaining = Timers.GetTimerRemaining(FireHandle); // -1 if not found
Timers.ClearTimer(FireHandle); // invalidates the handle
Timers.ClearAllTimersForObject(this); // clears timers bound to this object; CreateLambda/TFunction timers need ClearTimer(Handle)
FTimerDelegate は TDelegate<void(), FNotThreadSafeNotCheckedDelegateUserPolicy> (TimerManager.h:23) です。CreateUObject、CreateWeakLambda、CreateLambda、CreateSP、CreateStatic (Delegates/DelegateSignatureImpl.inl) で作成します。SetTimer のオーバーロードでは、メソッドポインター、FTimerDelegate、FTimerDynamicDelegate、TFunction<void(void)>&&、またはコールバックなし(ハンドルのみのカウントダウン)を指定できます。Blueprint から使用する場合:UKismetSystemLibrary::K2_SetTimer(UObject* Object, FString FunctionName, float Time, bool bLooping, bool bMaxOncePerFrame = false, float InitialStartDelay = 0.f, float InitialStartDelayVariance = 0.f) および K2_ClearAndInvalidateTimerHandle(const UObject* WorldContextObject, UPARAM(ref) FTimerHandle& Handle) (Kismet/KismetSystemLibrary.h:902,832)。タイマーの例:threading-patterns.md。
スレッドセーフティのルール
- ゲームスレッドのみ:
UPROPERTYの読み取りまたは書き込み、UFUNCTIONの呼び出し、GetWorld()、スポーン、破棄、コンポーネントの変更、UObject のデリゲート、タイマー、ティッカー。エントリーポイントをcheck(IsInGameThread())で保護してください。 - 遅延処理に生の
UObject*やthisをキャプチャしないでください。TWeakObjectPtr<T>(UObject/WeakObjectPtrTemplates.h:25)をキャプチャし、ゲームスレッド上でGet()を使って解決するか、CreateWeakLambdaでデリゲートを作成してください。 - 起動からコールバックまでの間に GC が実行される可能性があります。
FGCScopeGuard(UObject/GarbageCollection.h:117)はスコープ内の GC をブロックします。ワーカーから短時間の読み取り専用アクセスを行う場合に限って使用し、ブロッキング待機を囲まないでください。 - レンダースレッド:
ENQUEUE_RENDER_COMMAND(MyCommand)([Data](FRHICommandListImmediate& RHICmdList) { UploadOnRenderThread(RHICmdList, Data); });(RenderCore/Public/RenderingThread.h:1087、モジュールRenderCore)。ゲームスレッドからFlushRenderingCommands()を呼び出すと、キューを処理します。IsInRenderingThread()で確認してください。 - スレッドセーフな
TSharedPtrの中であっても、共有データには独自のロックが必要です。 参照カウントはアトミックですが、ペイロードはアトミックではありません。 - ロックフリーの受け渡しを優先してください:
TMpscQueue、std::atomic、ダブルバッファリング、またはリソースごとに 1 つのFPipeを使用します。
完全なパターン、ロック順序、ダブルバッファリング、FScopedSlowTask、サニタイザーに関する注意事項については、thread-safety-guide.md を参照してください。非同期アセット読み込み(FStreamableManager::RequestAsyncLoad)は ue-data-assets-tables に属します。
非推奨 — 使用しないでください
| 出力しないもの | 5.8 で使用するもの | 出典 |
|---|---|---|
FTicker::GetCoreTicker() | FTSTicker::GetCoreTicker() | 5.8 のヘッダーに FTicker はなく、FTSTicker のみが存在します(Containers/Ticker.h:26) |
FThreadSafeCounter | std::atomic<int32> | ヘッダーコメント「DEPRECATED. Please use std::atomic<int32>」(HAL/ThreadSafeCounter.h:9) |
FThreadSafeBool | std::atomic<bool> | ヘッダーコメント「DEPRECATED」(HAL/ThreadSafeBool.h:9) |
TAtomic<T> | std::atomic<T> | 「planned for deprecation」(Templates/Atomic.h:13、:528)。UE_DEPRECATED マクロはまだありません |
FExternalMutex | TIntrusiveMutex<Params>(Async/IntrusiveMutex.h:60) | Async/ExternalMutex.h:73 で UE_DEPRECATED(5.7) |
TExternalMutex<Params> | TIntrusiveMutex<Params> | Async/ExternalMutex.h:23 で UE_DEPRECATED(5.8) |
FPlatformProcess::CreateSynchEvent(...) | GetSynchEventFromPool / ReturnSynchEventToPool、または FEventRef | GenericPlatform/GenericPlatformProcess.h:776 で UE_DEPRECATED(5.0) |
TQueue<T, EQueueMode::Mpsc> / EQueueMode::Spsc | TMpscQueue<T> / TSpscQueue<T> | 「planned for deprecation」(Containers/Queue.h:11) |
ParallelFor(Num, Body, bool bForceSingleThread, bool bPumpRenderingThread) | ParallelFor(Num, Body, EParallelForFlags) | bool オーバーロードは Async/ParallelFor.h:481 に残っています。フラグ形式の :526 がドキュメントで推奨されています |
AsyncPool(GThreadPool, ...) | AsyncPool(*GThreadPool, ...) | パラメーターは FQueuedThreadPool& です(Async/Async.h:407)。ポインター形式ではコンパイルできません |
「デフォルトはスレッドセーフではないため」という理由で TSharedPtr<T, ESPMode::NotThreadSafe> を使うこと | TSharedPtr<T> — デフォルトは ESPMode::ThreadSafe です | Templates/SharedPointerFwd.h:25 |
新しい処理に TGraphTask<T> / FGraphEventRef を使うこと | UE::Tasks::Launch / FTask(非推奨ではなく推奨) | Async/TaskGraphInterfaces.h に UE_DEPRECATED はありません。FGraphEventRef は UE::Tasks の前提条件として引き続き受け入れられます(Tasks/Task.h:360) |
よくある間違い
ワーカースレッドから UObject に触れること: ガベージコレクションや、ゲームスレッドからのその他の書き込みと競合します。
// WRONG — Health is a UPROPERTY on this AMyActor
Async(EAsyncExecution::ThreadPool, [this]() { Health = ComputeHealth(); });
// RIGHT
Async(EAsyncExecution::ThreadPool, [Weak = TWeakObjectPtr<AMyActor>(this)]()
{
const float NewHealth = ComputeHealth();
AsyncTask(ENamedThreads::GameThread, [Weak, NewHealth]() { if (AMyActor* A = Weak.Get()) { A->ApplyResult(NewHealth); } });
});
起動直後にゲームスレッドをブロックすること: 次の行で Task.GetResult() または Future.Get() を呼び出すと、非同期処理が同期処理になってしまいます。Tick で IsCompleted()/IsReady() をポーリングするか、Prerequisites で連鎖させるか、AsyncTask(ENamedThreads::GameThread, Lambda) でゲームスレッドに戻します。
FNonAbandonableTask 内のプライベートメンバー: friend class FAsyncTask<T> が対象にするのはコンストラクター(FAsyncTask 内で実行されます)だけで、コードから読み取る GetTask().Result は対象外です。結果メンバーはパブリックにしてください。
FRWLock の入れ子取得: FRWLock は再帰的ではありません。読み取りロック中に読み取りロックを取得したり(または読み取りロック中に書き込みロックを取得したり)すると、デッドロックが発生します。呼び出し経路ごとに一度だけ取得するか、FCriticalSection に切り替えてください。
ParallelFor 内の共有可変状態:
TArray<int32> Data;
// WRONG
int32 Total = 0; ParallelFor(Data.Num(), [&](int32 i) { Total += Data[i]; });
// RIGHT
std::atomic<int32> Total{ 0 }; ParallelFor(Data.Num(), [&](int32 i) { Total.fetch_add(Data[i], std::memory_order_relaxed); });
処理中に FPipe または FRunnable の所有者を破棄すること: デストラクター本体が実行される前に、Pipe.WaitUntilEmpty() と Thread->Kill(true) を呼び出してください。~FPipe() は !HasWork() をアサートします。
FRunnable::Stop() 内でブロックすること: Run() の実行中に、Stop() は呼び出し元のスレッドで実行されます。アトミックフラグを設定するかイベントをトリガーするだけにして、その後 Kill(true) で待機してください。
破棄されるアクターに対する生のデリゲートタイマー: FTimerDelegate::CreateLambda([this]{}) は EndPlay 後も呼び出し続けます。CreateWeakLambda/CreateUObject を使用し、ClearAllTimersForObject(this) を呼び出してください。
関連スキル
ue-cpp-foundations—TSharedPtr/TWeakObjectPtr/TStrongObjectPtrのセマンティクス、GC のライフタイム、サブシステムの一覧ue-data-assets-tables—FStreamableManager、UAssetManager、アセットの非同期読み込みとソフト参照ue-testing-debugging— Unreal Insights のタスクおよびスレッドのトレース、statコマンド、非同期コードのログとオートメーションテストue-procedural-generation—FAsyncTask/UE::Tasksでの長時間実行される生成処理、ゲームスレッドへのProceduralMeshComponentの受け渡しue-mass-entity—ParallelForEachEntityChunk、EParallelExecutionFlags、プロセッサーのスレッド化ルールue-networking-replication— RPC およびレプリケーションのコールバックは常にゲームスレッドで実行されますue-blueprint-cpp-interop— C++ の Blueprint への公開:UFUNCTION/UPROPERTY メタキー、潜在アクションおよび非同期ノードue-niagara-effects— Niagara システム、ユーザーパラメーター、データインターフェースおよびデータチャンネルue-serialization-savegames— USaveGame、FArchive、アクターのスナップショットおよび設定の永続化