UE5 C++ 멀티스레딩 가이드
개요 — UE 멀티스레딩의 필요성과 제약
Section titled “개요 — UE 멀티스레딩의 필요성과 제약”CPU 집약적 작업(파일 IO, 경로 탐색, 물리 계산)을 게임 스레드(Game Thread)에서 처리하면 프레임 드랍이 발생합니다. 백그라운드 스레드로 분리해 게임 스레드를 해방시켜야 합니다.
핵심 제약:
- UObject, Actor, Component는 반드시 게임 스레드에서만 접근해야 합니다.
- 렌더 명령은 렌더 스레드에서만 실행합니다.
- 백그라운드 작업 완료 후 게임 스레드로 결과를 전달할 때는
AsyncTask(ENamedThreads::GameThread, ...)를 사용합니다.
1. 접근 방법 비교
Section titled “1. 접근 방법 비교”| 방법 | 적합한 상황 | 복잡도 |
|---|---|---|
FRunnable | 장기 실행 루프 (서버 연결, 스트리밍) | 중간 |
AsyncTask / Async | 일회성 백그라운드 작업 | 낮음 |
| TaskGraph | 의존성 있는 작업 그래프 | 높음 |
TFuture / TPromise | 비동기 결과 반환 | 낮음 |
2. FRunnable — 장기 실행 스레드
Section titled “2. FRunnable — 장기 실행 스레드”FRunnable은 독립 스레드에서 루프를 실행하는 가장 저수준 방법입니다.
2.1 FRunnable 구현
Section titled “2.1 FRunnable 구현”#pragma once
#include "CoreMinimal.h"#include "HAL/Runnable.h"
class FFileLoaderRunnable : public FRunnable{public: FFileLoaderRunnable(const FString& FilePath, TFunction<void(TArray<uint8>)> OnComplete); virtual ~FFileLoaderRunnable();
// FRunnable 인터페이스 virtual bool Init() override; virtual uint32 Run() override; // 스레드 메인 함수 virtual void Stop() override; // 외부에서 중단 요청 virtual void Exit() override; // 스레드 종료 후 정리
void EnsureCompletion();
private: FString FilePath; TFunction<void(TArray<uint8>)> OnCompleteCallback;
FRunnableThread* Thread = nullptr; FThreadSafeCounter StopRequested; // 원자적 플래그};#include "FileLoaderRunnable.h"#include "HAL/RunnableThread.h"#include "Async/AsyncWork.h"#include "Misc/FileHelper.h"
FFileLoaderRunnable::FFileLoaderRunnable(const FString& InPath, TFunction<void(TArray<uint8>)> InCallback) : FilePath(InPath) , OnCompleteCallback(InCallback){ // 스레드 생성 (자동 실행 시작) Thread = FRunnableThread::Create(this, TEXT("FileLoaderThread"), 0, TPri_BelowNormal);}
FFileLoaderRunnable::~FFileLoaderRunnable(){ if (Thread) { Thread->Kill(true); delete Thread; Thread = nullptr; }}
bool FFileLoaderRunnable::Init(){ return true; // 초기화 성공 시 Run() 진입}
uint32 FFileLoaderRunnable::Run(){ // 백그라운드 스레드에서 파일 로드 TArray<uint8> FileData; if (!StopRequested.GetValue() && FFileHelper::LoadFileToArray(FileData, *FilePath)) { // 완료 후 게임 스레드로 결과 전달 (중요!) AsyncTask(ENamedThreads::GameThread, [this, Data = MoveTemp(FileData)]() mutable { // 게임 스레드에서 콜백 실행 if (OnCompleteCallback) { OnCompleteCallback(MoveTemp(Data)); } }); } return 0;}
void FFileLoaderRunnable::Stop(){ StopRequested.Set(1); // 원자적 중단 플래그}
void FFileLoaderRunnable::Exit(){ // 스레드 종료 후 정리 작업}
void FFileLoaderRunnable::EnsureCompletion(){ Stop(); if (Thread) { Thread->WaitForCompletion(); }}// 사용 예시void AMyActor::BeginPlay(){ Super::BeginPlay();
FileLoader = MakeUnique<FFileLoaderRunnable>(TEXT("D:/data.bin"), [this](TArray<uint8> Data) { // 이미 게임 스레드에서 실행됨 UE_LOG(LogTemp, Log, TEXT("File loaded: %d bytes"), Data.Num()); ProcessData(MoveTemp(Data)); });}
void AMyActor::EndPlay(const EEndPlayReason::Type Reason){ if (FileLoader) { FileLoader->EnsureCompletion(); FileLoader.Reset(); } Super::EndPlay(Reason);}3. AsyncTask / Async — 일회성 비동기 작업
Section titled “3. AsyncTask / Async — 일회성 비동기 작업”3.1 AsyncTask (Low-level)
Section titled “3.1 AsyncTask (Low-level)”#include "Async/AsyncWork.h"
// 백그라운드 스레드로 작업 전송AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [](){ // 백그라운드 스레드에서 실행 int32 Result = HeavyComputation();
// 게임 스레드로 결과 반환 AsyncTask(ENamedThreads::GameThread, [Result]() { // 게임 스레드에서 UObject 안전 접근 UE_LOG(LogTemp, Log, TEXT("Result: %d"), Result); });});3.2 Async (고수준, 추천)
Section titled “3.2 Async (고수준, 추천)”#include "Async/Async.h"
// TFuture 반환 — 결과를 나중에 받을 수 있음TFuture<int32> Future = Async(EAsyncExecution::ThreadPool, []() -> int32{ return HeavyComputation(); // 백그라운드에서 실행});
// 게임 스레드에서 결과 사용 (블로킹)// int32 Result = Future.Get();
// 비블로킹: Then으로 완료 콜백Future.Next([](int32 Result){ // 별도 스레드에서 실행될 수 있음 — UObject 접근 주의 UE_LOG(LogTemp, Log, TEXT("Async result: %d"), Result);});3.3 FAsyncTask — 작업 클래스 기반
Section titled “3.3 FAsyncTask — 작업 클래스 기반”class FPathfindingTask : public FNonAbandonableTask{ friend class FAutoDeleteAsyncTask<FPathfindingTask>;
public: FPathfindingTask(FVector Start, FVector Goal, TFunction<void(TArray<FVector>)> Callback) : StartPos(Start), GoalPos(Goal), OnComplete(Callback) {}
void DoWork() { // 백그라운드에서 경로 탐색 TArray<FVector> Path = ComputePath(StartPos, GoalPos);
// 게임 스레드로 결과 전달 AsyncTask(ENamedThreads::GameThread, [this, Path = MoveTemp(Path)]() mutable { if (OnComplete) OnComplete(MoveTemp(Path)); }); }
FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FPathfindingTask, STATGROUP_ThreadPoolAsyncTasks); }
private: FVector StartPos; FVector GoalPos; TFunction<void(TArray<FVector>)> OnComplete;
TArray<FVector> ComputePath(FVector Start, FVector Goal) { return {}; } // 구현};
// 사용void AMyAI::RequestPath(FVector Start, FVector Goal){ // FAutoDeleteAsyncTask: 완료 후 자동 삭제 (new FAutoDeleteAsyncTask<FPathfindingTask>(Start, Goal, [this](TArray<FVector> Path) { // 게임 스레드에서 호출됨 NavigatePath(Path); }))->StartBackgroundTask();}4. 동기화 기법
Section titled “4. 동기화 기법”4.1 FCriticalSection (Mutex)
Section titled “4.1 FCriticalSection (Mutex)”FCriticalSection DataLock;TArray<int32> SharedData;
// 쓰기 (다른 스레드에서){ FScopeLock Lock(&DataLock); // 스코프 내 자동 잠금/해제 SharedData.Add(42);}
// 읽기 (게임 스레드에서){ FScopeLock Lock(&DataLock); for (int32 Value : SharedData) { UE_LOG(LogTemp, Log, TEXT("%d"), Value); }}4.2 FThreadSafeCounter — 원자적 정수
Section titled “4.2 FThreadSafeCounter — 원자적 정수”FThreadSafeCounter PendingTaskCount;
// 작업 시작 시PendingTaskCount.Increment();
// 작업 완료 시if (PendingTaskCount.Decrement() == 0){ // 모든 작업 완료 AsyncTask(ENamedThreads::GameThread, [this]() { OnAllTasksComplete(); });}4.3 FEvent — 신호 대기
Section titled “4.3 FEvent — 신호 대기”FEvent* ReadyEvent = FPlatformProcess::GetSynchEventFromPool(false);
// 스레드 A: 신호 대기ReadyEvent->Wait();
// 스레드 B: 신호 전송ReadyEvent->Trigger();
// 사용 후 반환FPlatformProcess::ReturnSynchEventToPool(ReadyEvent);5. 게임 스레드 안전성 체크
Section titled “5. 게임 스레드 안전성 체크”// 현재 게임 스레드인지 확인void UMyObject::SafeUpdate(){ if (!IsInGameThread()) { // 게임 스레드로 전달 AsyncTask(ENamedThreads::GameThread, [this]() { SafeUpdate(); }); return; }
// 이 이하는 게임 스레드에서 실행됨 // UObject / Actor 접근 안전 SomeActor->SetActorLocation(NewLocation);}| 기법 | 적합한 상황 |
|---|---|
FRunnable | 스트리밍·서버 연결 등 지속 루프 |
Async / AsyncTask | 일회성 헤비 계산 (경로, 파싱) |
FAsyncTask | 재사용 가능한 작업 단위 클래스 |
FCriticalSection | 공유 데이터 보호 |
FThreadSafeCounter | 원자적 카운터 (진행 추적) |
핵심 규칙:
- UObject·Actor는 게임 스레드에서만 접근 — 백그라운드에서 UObject를 건드리면 크래시
- 백그라운드 결과를 게임 스레드로 전달할 때는 반드시
AsyncTask(ENamedThreads::GameThread, ...)사용 FRunnableThread소멸 전WaitForCompletion()으로 완전히 종료 확인