게임 개발을 하다보면 성능 최적화는 피할 수 없는 문제다.
대부분의 작업이 메인 스레드에서 돌아갈 때, 이를 여러 쓰레드에 병렬로 부담을 분산시키면 많은 최적화를 이뤄낼 수 있다.
게임 개발에서 중요한 언리얼 엔진 5의 멀티스레딩 시스템에 대해 정리해보려 한다.
1. Multi-threading이란?
멀티스레딩은 프로세스 내에서 여러 실행 흐름을 만들어 CPU 자원을 최대한 활용하는 기술이다.
메인 스레드(Game Thread)의 부하를 분산해서 여러 코어에서 동시에 연산을 수행한다.
- 물리적으로 여러 코어에서 동시에 작업을 처리할 수 있다
- 하지만 스레드 생성과 컨텍스트 스위칭에도 비용이 든다
- 작업 단위가 너무 작으면 오히려 단일 스레드보다 느려질 수 있다
2. 동기화 프리미티브
멀티스레드 환경에서 여러 스레드가 같은 데이터에 접근하면 문제가 생긴다. 데이터 무결성을 보장하기 위한 도구들을 살펴보자.
| 도구 | 설명 | 용도 |
| FCriticalSection | 상호 배제(Mutex) 구현 | FScopeLock과 함께 쓰면 자동 해제됨 |
| TAtomic<T> | 원자적 연산 보장 | i++ 같은 간단한 연산 보호 |
| FRWLock | Read-Many, Write-One | 읽기는 많고 쓰기는 가끔일 때 효율적 |
*Race Condition: 두 개 이상의 스레드가 공유 자원에 동시 접근해서 결과가 꼬이는 현상
*Deadlock: 서로가 점유한 자원을 무한히 기다리는 교착 상태. 자원 획득 순서를 일관되게 설계하면 방지할 수 있다.
3. 언리얼 엔진 5 스레드 구조
언리얼은 작업 중심(Task-oriented) 병렬 구조를 쓴다.
Game Thread
게임 로직, 액터 업데이트, 가비지 컬렉션이 일어나는 메인 스레드다.
대부분의 UObject 접근은 여기서만 안전하다.
Render Thread
Game Thread로부터 데이터를 받아서 렌더링 커맨드를 생성한다.
RHI Thread
생성된 커맨드를 GPU API(DX12, Vulkan)와 직접 통신하는 레이어다.
Worker Threads
애니메이션, 물리 연산, 백그라운드 태스크를 처리하는 스레드 풀이다.
4. Unreal에서의 Multi-threading
4-1. FRunnable
FRunnable은 엔진에서 가장 원시적인 형태의 스레드 인터페이스다. OS 스레드를 직접 생성하고 관리해야 할 때 사용하며, 게임 루프와 무관하게 장시간 실행되어야 하는 작업에 최적화되어 있다.
- 주요 메서드 및 생명주기:
- Init(): 스레드가 생성된 후 가장 먼저 호출된다. 자원 할당 및 초기화를 수행하며, 실패 시 false를 반환하여 실행을 중단할 수 있다.
- Run(): 실제 로직이 수행되는 핵심 루프다. 여기서 무한 루프(while(!bStopThread))를 돌며 작업을 수행한다.
- Stop(): 스레드 중단을 요청할 때 호출된다. 원자적 변수(Atomic)를 통해 Run() 내의 루프를 안전하게 종료시킨다.
- Exit(): 스레드가 완전히 종료된 후 호출되는 정리(Cleanup) 단계다.
- 구현 시 고려사항:
- Thread 관리: FRunnableThread::Create()를 통해 인스턴스를 생성하며, 사용이 끝나면 Kill()을 호출하여 정리해야 한다.
- 사용 예시: TCP/UDP 소켓 리스너, 백그라운드 파일 IO 스트리밍, 전용 데이터베이스 연동.
4-2. Task Graph
Task Graph는 작업을 '태스크' 단위로 쪼개고, 각 태스크 간의 선후 관계(Dependency)를 정의하여 엔진의 워커 스레드 풀에서 실행하게 하는 시스템이다.
- 핵심 특징:
- 의존성(Prerequisites) 관리: "A와 B가 완료된 후에만 C를 실행하라"는 로직을 FGraphEventRef를 통해 쉽게 구현할 수 있다.
- 명명된 스레드(Named Threads): 특정 작업을 반드시 GameThread나 RenderThread에서 실행하도록 강제할 수 있다. 이는 메인 스레드 데이터에 접근해야 할 때 매우 유용하다.
- 내부 동작 원리:
- 엔진은 CPU 코어 수에 맞춰 워커 스레드를 생성해둔다.
- TGraphTask<FMyTask>::CreateTask()를 통해 큐에 작업을 넣으면, 스케줄러가 우선순위와 의존성을 판단하여 적절한 코어에 배분한다.
- 사용 예시: 애니메이션 병렬 업데이트, 피지컬 시뮬레이션 분산 처리, 대량의 액터 데이터 가공.
4-3. UE::Tasks
UE 5.x 버전부터 적극적으로 도입된 최신 시스템이다. 기존 AsyncTask나 Task Graph보다 가벼우며, 현대 C++의 std::future와 유사한 인터페이스를 제공한다.
- 가독성: 람다(Lambda) 식을 활용하여 코드 본문에 비동기 로직을 직관적으로 작성할 수 있다.
- 파이프(Pipe): 특정 작업군이 병렬로 실행되되, 서로 순서가 꼬이지 않게(Serial하게) 실행되도록 보장하는 Pipe 기능을 제공한다.
- 성능: Task Graph보다 오버헤드가 적도록 설계되어, 매우 작은 단위의 작업(Small Tasks)을 수만 개 실행할 때 유리하다.
- 사용 예시: 일회성 계산 작업, 로딩 화면 중 데이터 전처리, UE::Tasks::Pipe를 활용한 순차적 리소스 해제.
// 기본 실행
TTask<int32> MyTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [] { return 42; });
// 의존성 설정
TTask<void> NextTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [] { /* Do something */ }, MyTask);
추후 예시 코드와 함께 수정할 예정
https://dev.epicgames.com/community/learning/tutorials/BdmJ/unreal-engine-multithreading-techniques
Unreal Engine Multithreading Techniques | Community tutorial
Unreal Engine Multithreading Techniques is a concise 27-minute tutorial designed to introduce the learner to the essentials of multithreading in Unreal ...
dev.epicgames.com
https://www.youtube.com/watch?v=XJMyNM8xmS0
'게임개발 > Unreal Engine' 카테고리의 다른 글
| [UE5] FName, FText, FString (0) | 2026.03.28 |
|---|---|
| [UE5] Replicate Montage Multicast in C++ not working (0) | 2025.03.01 |
| [UE5] C++ AActor::Destroy() not working in BeginPlay() (0) | 2025.02.06 |
| [UE5] Unreal Engine C++ API References (2) | 2024.08.20 |
| [UE5] Root Motion with custom mesh not working (0) | 2024.07.17 |