[Unreal]#24_ParallelFor
Unreal 개발 중 "ParallelFor"에 대해 알아보겠습니다.
Overview
- 개념
- 코드
- 적용 사례
#0. 개념
1. Task Graph??
- Unreal Engine의 Task System은 "Task Graph"로 사용자가 작성한 게임 플레이 코드를 비동기적으로 실행할 수 있도록 해주는 Job Manager입니다.
- Task Graph는 방향성 비 순환 그래프입니다. 말 그대로, 방향성 비 순환 그래프는 개별 요소들이 특정한 방향을 갖고, 서로 순환하지 않는 그래프입니다.
- Task Graph는 Task의 비동기적 처리, 처리에 대한 결과 대기, Prerequisite 지정(전제 조건, 혹은 어떤 Task가 실행을 모두 마쳐야 실행 가능한 Task를 의미), Nested Task 실행, Task Chain 구성, 그리고 Task Event 사용을 통한 동기화 작업 등을 수행합니다.
2. ParallelFor??
#include "Runtime/Core/Public/Async/ParallelFor.h"
/**
* General purpose parallel for that uses the taskgraph
* @param Num; number of calls of Body; Body(0), Body(1)....Body(Num - 1)
* @param Body; Function to call from multiple threads
* @param bForceSingleThread; Mostly used for testing, if true, run single threaded instead.
* Notes: Please add stats around to calls to parallel for and within your lambda as appropriate. Do not clog the task graph with long running tasks or tasks that block.
**/
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, bool bForceSingleThread, bool bPumpRenderingThread=false)
{
ParallelForImpl::ParallelForInternal(Num, Body,
(bForceSingleThread ? EParallelForFlags::ForceSingleThread : EParallelForFlags::None) |
(bPumpRenderingThread ? EParallelForFlags::PumpRenderingThread : EParallelForFlags::None));
}
Details
- ParallelFor 함수는 TaskGraph를 활용해 동기화 프로그래밍을 지원합니다.
- Graph Task는 종종 GameThread를 사용하기도 합니다. 따라서, 모든 Task가 멀티 쓰레드로 수행되지 않을 수 있죠. 그래서 우리는 ParallelFor를 간단한 작업 용도로 사용해야 합니다.
#1. 코드
1. 헤더
#include "Runtime/Core/Public/Async/ParallelFor.h"
2. 사용 방법
for(auto Element : Array)
{
++Element;
}
#include "Runtime/Core/Public/Async/ParallelFor.h"
ParallelFor(Array.Num(), [&](uint8 Idx){
++Array[Idx];
})
Details
- 첫 번째 예제 코드는 간단한 범위 기반 반복문을 활용합니다.
- 두 번째 예제는 ParallelFor를 활용한 비동기 프로그래밍입니다.
3. Precaution
// Race Condition 발생!!
ParallelFor(Input.Num(), [&](int32 Idx)
{
if(Input[Idx]% 5 == 0)
{
Output.Add(Input[Idx]);
}
});
Details
- ParallelFor는 동기화 프로그래밍을 지원합니다.
- 따라서, ParallelFor 에서 Loop 밖에 있는 변수에 읽기 혹은 쓰기 작업을 수행할 경우 Race Condition이 발생할 수 있습니다!
FCriticalSection Mutex;
ParallelFor(Input.Num(), [&](int32 Idx)
{
if(Input[Idx] % 5 == 0)
{
Mutex.Lock();
Output.Add(Input[Idx]);
Mutex.Unlock();
}
});
Details
- 동기화 프로그래밍이 갖는 문제점을 해결하기 위해 우리는 "Mutex"를 활용합니다.
- FCriticalSection 변수를 통해 Critical Section(Race Condition이 발생할 수 있는 지점)의 시작 지점에서 Lock()을 호출하고, 모든 작업이 완료하는 시점에 Unlock()을 호출해줍니다.
- 기본적으로, ParallelFor을 활용해 배열 전체 항목들의 합을 구하는 등의 작업은 피해야 합니다!
#2. 적용 사례
1. UC_ProjectileSpawner::BeginPlay()
void UC_ProjectileSpawnerComponent::BeginPlay()
{
Super::BeginPlay();
// #0. Load Projectile Info Data Table
LoadProjectileInfoFromDataTable();
// #1. Projectile Pool Init
for (uint8 i = 0; i < ProjectileInfo_Structs.Num(); i++)
{
TArray<AProjectile*> TmpArray;
TmpArray.Init(nullptr, ProjectileInfo_Structs[i].Max_PoolSize);
ProjectilePool.Add(TmpArray);
PoolHeads.Add(&ProjectilePool[i][0]);
}
// #2. Create Projectile Pool
CreatePool();
// #3. Damage Delegate
ParallelFor(ProjectileInfo_Structs.Num(), [&](uint8 Idx) {
for (const auto& Projectile : ProjectilePool[Idx])
{
Projectile->OnProjectileBeginOverlap.BindUFunction(this, "EndLaunchedProjectile");
}
});
}
Details
- Projectile Spawner 컴포넌트는 무기의 발사체를 관리하는 컴포넌트입니다.
- 기본적으로, Projectile 클래스마다 Object Pooling 여부를 확인하고, 게임 시작 시점에 각 Projectile 클래스 별 Object Pool을 생성합니다.
- 이때, Projectile 객체의 Damage 전달을 관리하는 Delegate 함수를 호출하는 코드에 ParallelFor을 활용했습니다.