#1. 개념
1. 비동기 프로그래밍
비동기 프로그래밍은 프로그램의 주 실행 흐름을 차단하지 않고, 작업을 병렬적으로 수행할 수 있게 해주는 프로그래밍 패러다임입니다. 이는 이전 작업의 완료를 기다리지 않고, 병렬적으로 작업들을 수행하며 프로그램의 응답성 향상과 리소스의 효율적인 활용을 목표로 합니다. 다만, 비동기 프로그래밍은 코드 복잡성 증가, 디버깅 어려움, 그리고 오 사용 시 발생하는 race condition 등의 몇 가지 단점 혹은 주의할 점이 있습니다.
2. 특징
- 비 차단 실행: 시간이 오래 걸리는 작업을 호출한 뒤, 해당 작업의 완료까지 기다리지 않고 즉시 다음 작업을 실행합니다. 이를 통해, 프로그램의 응답성을 향상할 수 있습니다.
- 병렬성: 여러 작업을 동시에 수행합니다.
- Promise/Future 패턴: 비동기 작업의 결과를 나중에 제공하겠다는 약속(Promise), 약속에 의해 설정될 값을 나타내는 객체(Future)로 구성된 이 패턴을 활용할 수 있습니다.
- Event Loop: 비동기 작업의 완료를 감지하고 적절한 콜백을 호출합니다. 주로, 싱글 스레드에서 활용됩니다.
- 비동기 함수: 'async' 혹은 'await' 키워드를 사용하여 비동기 코드를 동기 코드처럼 작성 가능합니다.
#2. AsyncTask
1. 개념
template<typename TFunction>
void AsyncTask(ENamedThreads::Type Thread, TFunction&& Function);
AsyncTask는 특정 명명된 스레드에서 함수나 람다(익명 함수 객체)를 비동기적으로 실행하는 템플릿 함수입니다.
첫 번째 인자(Thread)는 비동기 작업을 수행할 스레드를 지정하고, 두 번째 매개변수(Function)는 실행할 함수나 람다입니다. 특히, AsyncTask는 게임 스레드, 렌더링 스레드 등 사용자가 특정 스레드에서 비동기 작업을 수행할 수 있도록 해주며, 복잡한 설정 없이 빠르게 비동기 작업을 스케줄링할 수 있습니다.
2. Thread
- ENameThreads::GameThread: 메인 게임 로직이 실행되는 스레드
- ENameThreads::RenderingThread: 렌더링 관련 작업이 실행되는 스레드
- ENameThreads::AnyThread: 아무 스레드.
- 주의할점: UObject, 혹은 언리얼 엔진 객체를 조작할 경우 반드시 GameThread를 선택해야 합니다. 그리고, 비교적 실행 시간이 오래 걸리는 작업을 게임 스레드에서 처리할 경우 프레임 저하의 원인이 됩니다. 마지막으로, 가장 중요한 점은 race condition으로 동기화 작업이 필수적입니다.
3. 동작 방식
- FAuotDeleteAsyncTask: AsyncTask 호출 시 전달받은 함수나 람다는 즉시 실행되지 않고, FautoDeleteAsyncTask로 래핑 됩니다. 해당 객체는 작업 완료 시점에 메모리에서 제거됩니다.
- Thread Queue: 래핑 된 작업은 사용자가 지정한 스레드 큐에 추가됩니다.
- Execute: 래핑된 작업의 차례가 되면, 큐에서 해당 작업을 꺼내 실행됩니다.
- 삭제: 실행이 모두 완료된 FAutoDeleteAsyncTask는 메모리에서 자동으로 제거됩니다.
4. 코드
// YourActor.h
UCLASS()
class YOURPROJECT_API AYourActor : public AActor
{
GENERATED_BODY()
public:
AYourActor();
UFUNCTION(BlueprintCallable, Category = "Async")
void PerformAsyncTask();
private:
void OnTaskCompleted(int32 Result);
};
// YourActor.cpp
#include "YourActor.h"
#include "Async/Async.h"
AYourActor::AYourActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void AYourActor::PerformAsyncTask()
{
// 비동기 작업 시작
Async(EAsyncExecution::Thread, [this]()
{
// 시간이 오래 걸리는 작업 시뮬레이션
FPlatformProcess::Sleep(3.0f);
// 작업 결과 계산
int32 Result = FMath::RandRange(1, 100);
// 게임 스레드에서 결과 처리
AsyncTask(ENamedThreads::GameThread, [this, Result]()
{
OnTaskCompleted(Result);
});
});
UE_LOG(LogTemp, Log, TEXT("비동기 작업이 시작되었습니다."));
}
void AYourActor::OnTaskCompleted(int32 Result)
{
UE_LOG(LogTemp, Log, TEXT("비동기 작업이 완료되었습니다. 결과: %d"), Result);
}
#3. Promise/Future
1. 개념
//@Promise 객체
TPromise<typename T>Promise;
//@Future 객체
TFuture<typename T> Future = Promise.GetFuture();
//@Promise 값 설정
Promise.SetValue(Result);
//@Future 값 가져오기
typeName T Value = Future.Get();
//@Future에 콜백 등록
Future.Then([](typename T Value){
//...
});
Promise/Future 패턴은 비동기 프로그래밍을 위한 디자인 패턴으로 비동기 작업의 결과를 나중에 설정할 수 있는 객체(Promise)와 비동기 작업의 결과를 나중에 받을 수 있는 객체(Future)로 구성됩니다. Future는 Promise의 결과 값을 전달받는 것으로 약속하고, 결과 값을 활용한 콜백 함수 등록 등의 작업들을 수행할 수 있습니다.
2. 특징
- 비동기성: 작업의 시작과 결과 처리를 분리합니다.
- 일반화: 템플릿 활용을 통한 다형성 제공
3. 동작 방식
- Promise 생성/Future 획득: Promise 객체를 생성하고, 이를 통해 Future를 얻습니다.
- Async 호출: Async를 통해 비동기 작업을 수행합니다. 작업을 모두 마치고, 결과 값을 Promise에 설정해 줍니다.
- Future 활용: 비동기 작업의 결과 값을 예약한 Future를 통해 별도의 처리 작업을 수행합니다.
- 주의할 점: 부적절한 사용 시 성능 저하의 원인이 되고, 코드 복잡성 증가와 학습 곡선에 대한 주의가 필요합니다.
4. 코드
// YourActor.h
UCLASS()
class YOURPROJECT_API AYourActor : public AActor
{
GENERATED_BODY()
public:
AYourActor();
UFUNCTION(BlueprintCallable, Category = "Async")
void PerformComplexAsyncTask();
private:
void OnTaskCompleted(int32 Result);
void OnTaskFailed(const FString& Error);
};
// YourActor.cpp
#include "YourActor.h"
#include "Async/Async.h"
#include "Async/Future.h"
AYourActor::AYourActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void AYourActor::PerformComplexAsyncTask()
{
// Promise와 Future 생성
TPromise<int32> Promise;
TFuture<int32> Future = Promise.GetFuture();
// 비동기 작업 시작
Async(EAsyncExecution::Thread, [Promise = MoveTemp(Promise)]() mutable
{
// 시간이 오래 걸리는 작업 시뮬레이션
FPlatformProcess::Sleep(3.0f);
// 랜덤하게 성공 또는 실패 시뮬레이션
if (FMath::RandBool())
{
int32 Result = FMath::RandRange(1, 100);
Promise.SetValue(Result); // 성공 시 결과 설정
}
else
{
Promise.SetException(MakeShared<FString>("Task failed")); // 실패 시 예외 설정
}
});
// Future에 콜백 등록
Future.Then([this](TFuture<int32> CompletedFuture)
{
AsyncTask(ENamedThreads::GameThread, [this, CompletedFuture]()
{
if (CompletedFuture.IsReady())
{
try
{
int32 Result = CompletedFuture.Get();
OnTaskCompleted(Result);
}
catch (const TSharedPtr<FString>& Error)
{
OnTaskFailed(*Error);
}
}
});
});
UE_LOG(LogTemp, Log, TEXT("복잡한 비동기 작업이 시작되었습니다."));
}
void AYourActor::OnTaskCompleted(int32 Result)
{
UE_LOG(LogTemp, Log, TEXT("비동기 작업이 성공적으로 완료되었습니다. 결과: %d"), Result);
}
void AYourActor::OnTaskFailed(const FString& Error)
{
UE_LOG(LogTemp, Error, TEXT("비동기 작업이 실패했습니다. 에러: %s"), *Error);
}
#4. ParallelFor, ParallelReduceEx
1. ParallelFor
1. 개념
// 주의: 데이터 경쟁 예제
void DataRaceExample()
{
int32 SharedCounter = 0;
ParallelFor(1000000, [&](int32 Index)
{
// 잘못된 사용: 데이터 경쟁 발생 가능
SharedCounter++;
});
// 올바른 사용: 원자적 연산 사용
FThreadSafeCounter SafeCounter;
ParallelFor(1000000, [&](int32 Index)
{
SafeCounter.Increment();
});
}
ParallelFor는 Unreal Engine에서 제공하는 병렬 처리 함수로 범위 기반의 반복 작업을 여러 스레드에 분산하여 동시에 실행합니다. ParallelFor 활용을 통해 대량의 독립적인 작업을 빠르게 처리할 수 있으며, 내부적으로 작업을 여러 청크로 나누어 스레드 풀에 분배합니다. 특히, ParallelFor 함수는 람다 함수(익명 함수)가 활용 가능하다는 특징이 있습니다.
2. 동작 방식
- 분할: 전체 작업을 여러 청크로 분할합니다.
- 할당: 각 청크를 스레드 풀의 작업자 스레드에 할당합니다.
- 처리: 각 스레드는 할당받은 작업을 순차적으로 처리합니다.
- 완료: 모든 청크의 처리가 완료되면, ParellelFor 함수가 반환됩니다.
3. 장점/단점
- 멀티 코어 CPU의 효율적인 활용
- 대량의 데어터 처리 시간 단축
- 간단한 사용 방법
- 작업의 크기가 충분히 크지 않을 경우 오버헤드로 인한 성능 저하 우려
- 복잡한 의존성이 있는 작업에 부적합
- Race Condition을 고려해 동기화 작업이 필요합니다.
4. 코드
2. ParallelReduceEx
1. 개념
float ParallelReduceExExample()
{
TArray<float> Data;
Data.SetNum(1000000);
for (int32 i = 0; i < Data.Num(); ++i)
{
Data[i] = FMath::Sin(i * 0.01f);
}
float Sum = ParallelReduceEx(Data, 0.0f, [](float A, float B)
{
return A + B;
});
return Sum / Data.Num(); // 평균 계산
}
ParallelReduceEx는 병렬로 데이터를 처리하고, 그 결과를 집계하는 함수입니다. ParallelReduceEx는 MapReduce 패턴의 "Reduce"부분을 재현한 것으로 보입니다.
2. 특징
- 대량의 데이터를 병렬로 처리하고, 그 결과를 하나로 압축합니다.
- 사용자 정의 축소(Reduce)를 활용해 유연한 결과 집계가 가능합니다.
- 내부적으로 분할-정복 방식을 활용합니다.
3. 동작 방식
- 분할: 입력 데이터를 여러 청크로 분할
- 처리: 각 청크를 병렬로 처리하여 부분 결과 계산
- 결합: 사용자 정의 축소 함수를 통해 부분 해를 반복적으로 결합하여 최종 결과 값을 반환
4. 장점/단점
- 대량의 데이터의 집계 작업을 효율적으로 수행
- 사용자 정의 축소(Reduce) 함수를 통해 유연한 집계 방식 가능
- 내부적으로 최적화되어 있어 뛰어난 성능
- 데이터 크기가 충분히 크지 않다면 오버헤드로 인한 성능 저하
- 축소 함수는 결합 법칙과 교환 법칙을 만족해야 하며, 축소 함수 내 부작용이 없도록 설계해야 합니다.
'게임개발 > Unreal C++' 카테고리의 다른 글
[Unreal]#UI 최적화 (1) | 2024.08.28 |
---|---|
[Unreal]#UFUNCTION 매크로 (0) | 2024.08.28 |
[Unreal]#ESlateVisibility, UI 가시성 (0) | 2024.08.21 |
[Unreal]#Delegate, Event, Delegate vs Event (0) | 2024.07.30 |
[Unreal]#스마트 포인터 (0) | 2024.07.30 |