[Unreal Engine] Threading Classes

Imeamangryang·2025년 7월 4일

Unreal Multithreading

목록 보기
2/3
post-thumbnail

출처 : @Ayliroe : Multithreading and Performance in Unreal


Threading Classes

엔진의 Core에서 제공하는 대표적인 멀티스레딩 관련 클래스들을 간단히 정리하면 다음과 같습니다

FRunnable

Unreal에서 가장 포괄적인 스레딩 도구입니다. 아래와 같이 FRunnable을 상속받아 클래스를 구현하면 됩니다:

헤더 파일(.h) 소스 파일(.cpp)
// .h
#pragma once
#include "CoreMinimal.h"


class FMyThread : public FRunnable {
  FMyThread( /*Parameters*/ ) {
    Thread = FRunnableThread::Create(this, TEXT("MyThread"));
  };
  
  virtual bool Init() override;
  virtual uint32 Run() override;
  virtual void Exit() override;
  virtual void Stop() override;


  FRunnableThread* Thread;
  bool bShutdown= false;
};
// .cpp
#include "FMyThread.h"


bool FMyThread::Init() {
  /* Should the thread start? */
  return true;
}


uint32 FMyThread::Run() {
  while (!bShutdown) {
    /* Work on a dedicated thread */
  }
  return 0;
}


void FMyThread::Exit() {
  /* Post-Run code, threaded */
}


void FMyThread::Stop() {
  bShutdown = true;
}

스레드를 시작하려면 헤더를 포함하고 생성자를 호출하세요(포인터를 반드시 저장해두세요!):

auto* Thread = new FMyThread( /*Parameters*/ );

이 생성자는 내부적으로 FRunnableThread::Create()를 호출하며, this(즉, FMyThread)를 FRunnable로 넘깁니다.

클래스는 네 가지 함수를 오버라이드할 수 있습니다(자동으로 호출됨). 여기에 스레드에서 실행할 코드를 구현합니다:

  • Init(): FRunnableThread::Create()에 의해 호출되며, 게임 스레드에서 실행됩니다. 스레드 초기화 여부를 결정하는 로직을 넣고, 초기화가 필요하면 true를 반환하세요.
  • Run(): Init()이 완료된 후 새 스레드에서 실행됩니다. 실제 스레드 작업을 이 함수에 구현하세요. 함수가 끝나면 스레드가 종료되므로, 반복 작업이 필요하다면 루프를 두고, 종료 조건(예: bool 변수)을 직접 관리해야 합니다. 기본 반환값은 0(성공)입니다.
  • Exit(): Run()이 끝난 후 새 스레드에서 실행됩니다. 종료 시점에 필요한 추가 로직이 있다면 여기에 작성하세요.
  • Stop(): 게임 스레드에서 실행되는 일반 함수로, 자동으로 호출되지 않습니다. 스레드를 조기에 종료하고 싶을 때 Thread->Kill()을 호출하면 Stop()이 실행됩니다. 이때 while 루프를 멈추는 등 종료 방식을 직접 구현해야 합니다. Thread->Kill(false)로 강제 종료도 가능합니다.

이렇게 하면 별도의 독립 스레드에서 연산이 실행되며, 추가적인 작업 없이 자체적으로 동작합니다.

더 자세한 내용은 아래 링크를 참고해주세요.

UE5 Multithreading With FRunnable And Thread Workflow

Multithreading With FRunnable

Multi-Threading: How to Create Threads in UE4


AsyncTask

별도의 클래스를 만들거나 새로운 스레드를 시작하지 않고, 일시정지나 콜백 같은 제어 로직이 필요 없는 간단한 비동기 작업을 실행하고 싶다면, TaskGraph에서 실행되는 AsyncTask에 작업을 넣을 수 있습니다:

AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, [this] () {
  /* Work on the TaskGraph */
  Caller->FunctionToThread(); // Function call captured using [this]
});

구조는 TFunction과 유사하며, 이는 Unreal에서 C++ 람다를 구현하는 방식입니다.
따라서 AsyncTask 내부에서 외부에 선언된 변수나 함수를 사용하려면 반드시 참조(&)나 복사(=)로 캡처해야 하며, 멤버 변수는 보통 [this], 지역 변수는 [&]로 캡처합니다. (자세한 내용은 lambdas in C++ 참고).

ENamedThreads를 통해 작업을 실행할 스레드를 지정할 수 있으며, 게임 스레드 외부에서 호출할 경우 다시 게임 스레드로 작업을 넘길 수도 있습니다. TaskGraph에서 대부분 미리 생성된 태스크를 활용해 코드를 실행하므로, 짧은 태스크를 여러 번 큐에 넣는 것이 새로운 FRunnable 스레드를 만드는 것보다 오버헤드가 적지만, 실행이 다소 지연될 수 있습니다.


ParallelFor

AsyncTask보다 한 단계 발전된 방식으로, ParallelFor는 for 루프를 여러 개의 태스크로 분할하여 TaskGraph에서 병렬로 실행합니다.

ParallelFor(Array.Num(), [&](int32 i) { 
  // Run Array.Num() operations, with current index i
  /* Work on the TaskGraph (order of execution is variable!) */
  ++Array[i]; 
});

루프 내 작업의 순서나 스레드 안전성은 보장되지 않으므로, 필요한 경우 mutex나 atomic을 함께 사용하는 것이 좋습니다.
MSVC에서는 유사한 기능으로 #pragma loop(hint_parallel(n))을 제공합니다. 실제로는 루프 내 작업의 연산량이 충분히 클 때 이 방식의 이점을 체감할 수 있습니다.


FNonAbandonableTask

자체 AsyncTask를 선언하는 방법으로, FRunnable과 람다 기반 AsyncTask의 중간 형태입니다. 독립적인 클래스로 코드를 구현해 재사용성을 높일 수 있으며, 별도의 스레드가 아닌 TaskGraph에서 실행됩니다. 다만 FRunnable처럼 초기화(Init)나 정지(Stop)와 같은 라이프사이클 제어는 일부 제공되지 않습니다.

#pragma once
#include "CoreMinimal.h"


class FMyTask : public FNonAbandonableTask {
  friend class FAutoDeleteAsyncTask<FMyTask>;


  FMyTask( /*Parameters*/ ) {
    / Constructor */
  }
  void DoWork() {
    /* Work on the TaskGraph */
  }
  FORCEINLINE TStatId GetStatId() const { // Probably declares the Task to the TaskGraph
    RETURN_QUICK_DECLARE_CYCLE_STAT(FMyTask, STATGROUP_ThreadPoolAsyncTasks);
  }
};

커스텀 Task를 시작하려면 다음과 같이 작성하세요:

auto* MyTask = new FAsyncTask<FMyTask>( /*Parameters*/ );
MyTask->StartBackgroundTask();

태스크는 FAsyncTask 또는 FAutoDeleteAsyncTask의 friend로 선언할 수 있으며, 후자는 더 자동으로 관리됩니다.

더 자세한 내용은 아래 링크를 참고해주세요.

jdelezenne.github.io Multithreading

michaeljcole.github.io Using AsyncTasks

만약 블루프린트 코드를 C++로 옮기기 귀찮다면, AsyncTask를 통해 블루프린트 이벤트를 실행하는 방법도 있습니다.


FFunctionGraphTask and TGraphTask

FFunctionGraphTask는 람다를 사용해 태스크를 큐에 넣는 또 다른 방법입니다.
단일 선행 태스크(Prerequisite)와 완료 시 동작(Completion Action)을 옵션으로 지정할 수 있습니다.

FGraphEventRef PrerequisiteTask; // A task to be completed before


FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady( []() {
  /* Work on the TaskGraph */
}, TStatId(), PrerequisiteTask, ENamedThreads::AnyThread);


FGraphEventArray TasksList; TasksList.Add(Task);


if (Task->IsComplete()) {
  /* On work completed */
}

TGraphTask는 전체 사용자 정의 클래스를 태스크 그래프 큐에 추가하며, 필요에 따라 선행 태스크(prerequisites) 목록도 지정할 수 있습니다. 사용하려면 클래스 내에 DoTask() 함수를 직접 구현해야 합니다.

자세한 내용은 예제를 참고하세요.

FGraphEventRef Task = TGraphTask<FMyTask>::CreateTask(&PrerequisitesList, ENamedThreads::GameThread).ConstructAndDispatchWhenReady();

CreateTask()는 FConstructor를 반환하며, ConstructAndDispatchWhenReady()가 전달된 FMyTask를 생성합니다. 여기서 ENamedThreads는 태스크가 실행될 스레드가 아니라, 해당 호출이 이루어지는 스레드를 의미합니다.


AsyncPool, AsyncThread, TFuture and TPromise

엔진의 일부 기능은 공식 문서에 거의 언급되지 않으며, 아래 내용도 실제로는 직접 사용하는 용도가 아닐 수 있습니다.

AsyncPool은 미리 생성된 스레드 풀에서 함수를 실행하는 기능으로 보이며, 작업이 끝난 후 실행할 콜백 함수도 선택적으로 전달할 수 있습니다.

FQueuedThreadPool* Pool = FQueuedThreadPool::Allocate(); // Or Pool = GThreadPool;
verify(Pool->Create(4, 32 * 1024, TPri_Normal));

TUniqueFunction<void()> Callback = []() { // Wrap in MoveTemp() to pass as parameter
  /* Callback code */
};
TFunction<void()> Body = []() {
  / Work on a thread pool */
};

AsyncPool(*Pool, Body, MoveTemp(Callback));

FQueuedThreadPool은 다음과 같은 함수와 함께 사용되는 경우도 있습니다.

IQueuedWork* Work; Pool->AddQueuedWork(Work);

AsyncThread는 아마도 FRunnable과 비슷하게 새로운 스레드를 생성하며, 작업이 완료되면 TFuture 구조체를 반환할 수 있습니다.

TFunction<bool()> Task = []() {
  /* Work on a new thread */
  return true;
};
TFuture<bool> FutureResult = AsyncThread(Task);

TFuture와 TPromise의 사용법에 대해서는, 사실 저도 정확히는 잘 모릅니다. 대략적으로 콜백 시스템처럼 동작하며, Future.IsReady(), Future.IsValid(), Future = Promise.GetFuture() 같은 함수들이 제공됩니다.

더 자세한 내용은 아래 링크를 참고해주세요.

jdelezenne.github.io Multithreading

[WIP] Unreal Source Explained

profile
언리얼 엔진 주니어 개발자 입니다.

0개의 댓글