TIL_095 : 언리얼 스레드, 비동기 작업

김펭귄·2026년 1월 13일

Today What I Learned (TIL)

목록 보기
95/142

1. 언리얼 스레드

1.1. Game Thread(Main Thread)

  • 게임 로직 처리

  • 입력 처리

  • UI 업데이트

  • UObject 생성/삭제를 할 수 있는 유일한 스레드

// Enemy 스폰
AEnemy* NewEnemy = GetWorld()->SpawnActor<AEnemy>();
    
// UI 조작
EnemyCountWidget->SetText(FText::AsNumber(EnemyCount));
    
// 컴포넌트 추가/제거
UStaticMeshComponent* Mesh = NewObject<UStaticMeshComponent>(this);
    
// 대부분의 언리얼 API
UGameplayStatics::GetPlayerController(GetWorld(), 0);

1.2. Render Thread

  • 그래픽 담당

  • 드로우 콜 준비

  • 머티리얼 처리

  • 렌더 커맨드 생성

  • 게임 스레드보다 1프레임 뒤에 실행되어 게임 스레드가 데이터를 넘겨주면, 렌더링 스레드가 이를 받아 실제 GPU에 명령을 내림

1.3. RHI Thread

  • 렌더링 스레드에서 생성된 명령을 실제 그래픽 API(DirectX, Vulkan, Metal 등)에 맞게 변환하여 GPU로 전송하는 스레드

  • Render Thread의 부하를 덜어줌

1.4. Audio Thread

  • 사운드 믹싱, 오디오 이펙트 처리 등 실제 하드웨어로 소리를 보내는 역할

1.5. Worker Threads

  • 특정 역할이 고정되지 않은 범용 스레드 집합

  • 각 스레드 고유의 역할을 제외한 단순 작업은 어느 스레드에서 해도 상관 없어 이 작업자 스레드에서 실행

  • 스레드들의 부하를 덜어줄 수 있다

  • cpu 코어 개수에 맞게 미리 생성되어 있음 (Pooling)

  • 물리 계산, AI 경로 탐색, 파일 로딩, 기타 백그라운드 작업

  • 그러나, 각 스레드의 고유역할을 여기서 해버리면 프로그램이 터지니 애매하면 다른 스레드로 넘기지 말 것

2. 비동기 스레드 작업

  • Main Thread에서 작업도중 무거운 작업을 Worker Thread에게 맡기는 방법

  • UObjectWorld에 접근하는 작업은 무조건 Main Thread에서 해야함.
    그 이외의 무거운 작업만 맡겼다가, 다시 받아서 사용
    예) Actor, UI

  • 처음부터 멀티스레드를 사용하지 말고, 꼭 프로파일링 후 필요하면 멀티스레드 사용

2.1. Async-AsyncTask

  • 언리얼4부터 사용가능

  • ASync는 무거운 작업을 Worker Thread에 넘기는 것

  • ASyncTask는 특정 스레드에게 작업을 시키는 것

  • 그래서 보통 ASync로 Worker Thread에 작업을 넘기고, Worker Thread에서 AsyncTask로 완료한 작업을 MainThread에게 보내 처리함

  • 비동기이므로, Worker Thread는 백그라운드에서 작업을 계속 처리하고, Main Thread 역시 그 이후 작업을 하므로 게임이 멈추지 않고 계속 진행됨

#include "Async/Async.h"

// 모든 Enemy의 경로를 업데이트해주는 비동기 함수
void AEnemyManager::UpdateAllEnemyPathsAsync()
{
    // 1. 모든 Enemy이 경로 데이터를 담을 SharedPtr 생성
    TSharedPtr<TArray<FEnemyPathData>> SharedEnemyData = 
    	MakeShared<TArray<FEnemyPathData>>();
 
    for (AEnemy* Enemy : AllEnemies)
    {
        if (Enemy && Player)
        {
            FEnemyPathData Data;
            Data.EnemyID = Enemy->GetUniqueID();
            Data.StartLocation = Enemy->GetActorLocation();
            Data.TargetLocation = Player->GetActorLocation();
            SharedEnemyData->Add(Data);
        }
    }

    // 2. Worker Thread에 작업 맡김
    Async(EAsyncExecution::ThreadPool, [this, SharedEnemyData]()
    {
        // [Worker Thread 영역]
        TArray<FEnemyPathResult> PathResults;
			
        // 무거운 작업
        // const 참조를 사용하여 읽기 전용으로 접근
        for (const FEnemyPathData& Data : *SharedEnemyData)
        {
            FEnemyPathResult Result;
            Result.EnemyID = Data.EnemyID;
            Result.Path = FindPathAStar(Data.StartLocation, Data.TargetLocation);
            PathResults.Add(Result);
        }

        // 3. 작업 완료 후, ASyncTask불러 GameThread에게 결과물로 작업시킴
        AsyncTask(ENamedThreads::GameThread, [this, PathResults]()
        {
        	// 작업 결과 적용
            for (const FEnemyPathResult& Result : PathResults)
            {
                if (AEnemy* Enemy = FindEnemyByID(Result.EnemyID))
                {
                    Enemy->SetPath(Result.Path);
                }
            }
        });
    });
}
  • 비동기 작업 도중 Worker Thread로 넘긴 데이터가 사라지면 터짐

  • 따라서 TArray 자체를 공유 포인터로 감싸, 워커 스레드가 끝날 때까지 메모리가 유지되도록 함

  • 복사하고 스레드에 넘기기에는 데이터가 커서 복사비용으로 성능이 떨어질 수 있기 때문에, SharedPtr을 사용.
    또한, Main과 Worker에서 같이 접근해 Race Condition이 발생할 수 있어 Worker에선 const 참조자를 이용해 읽기만 가능하도록 설정

ASync 실행 옵션

  • EAsyncExecution::ThreadPool : 이미 만들어진 워커 풀 사용 (일반적이고 빠름). 보통 가볍고 자주 사용되는 작업에 적합

  • EAsyncExecution::Thread : 새 스레드를 생성. 다른 스레드에 간섭 없이 정말 무거운 작업을 혼자 처리하고 싶을 때 적합. 새로 생성하기에 좀 느림

  • EAsyncExecution::ThreadIfForkSafe : 조건부 스레드

  • EAsyncExecution::TaskGraph : 작업을 태스크 단위로 작게 나누어 병렬로(Graph) 처리

  • EAsyncExecution::TaskGraphMainThread : TaskGraph와 동일하나 다음 프레임에 게임 스레드를 예약

2.2. UE::Tasks

  • 언리얼 5부터 사용 가능

  • 작업을 맡기고 끝이 아니라, 여러 작업을 계속 이을 수 있음 (Chaining)

  • 병렬 작업이라 빠름

#include "Tasks/Task.h"

// Enemy 스폰 → 초기화 → AI 설정 체이닝
UE::Tasks::Launch(UE_SOURCE_LOCATION, 
    []() { return LoadEnemyData(); })
    .Then(UE_SOURCE_LOCATION, 
    [](auto Data) { return InitializeEnemies(Data); })
    .Then(UE_SOURCE_LOCATION, 
    [](auto Enemies) { SetupEnemyAI(Enemies); });

// Enemy 200마리 병렬 처리
TArray<UE::Tasks::FTask> EnemyTasks;
for (int i = 0; i < 200; ++i)
{
    EnemyTasks.Add(UE::Tasks::Launch(UE_SOURCE_LOCATION, [i]()
    {
        return ProcessEnemyAI(i);
    }));
}

UE::Tasks::WaitAll(EnemyTasks);  // EnemyTasks 작업이 끝날 때까지 대기
  • ASync, ASyncTask는 중간에 작업을 멈추기가 힘든데, Tasks는 작업 멈추는 것이 가능
profile
반갑습니다

0개의 댓글