https://dev.epicgames.com/documentation/ko-kr/unreal-engine/tasks-systems-in-unreal-engine
- GamePlay Code를 Async하게 실행할 수 있는 기능을 제공하는 Task Manager
- Dependent 작업에 대한 Directed Acyclic Graph를 만들고 실행할 수 있다.
- Unreal Engine의 Task Manager인 TaskGraph를 Implement해야 한다.
- 참고로 Task System과 Task Graph는 동일한 Back-end(Scheduler, Woker Thread)를 공유한다.
주요 기능
- Async하게 실행해야 하는 호출 가능 Object를 제공하는 Task 실행
- Task 완료 및 실행 결과 정보를 획득하는 동안 대기
- 선행 Task 지정
- Task 실행 전에 완료해야 하는 Task
- Task 내부에 중첩된 Task를 실행
- 부모 Task는 중촙 Task가 완료될 때까지 계속 실행이 된다.
- Pipe(Task Chain) 빌드
- Task간 Synchronize 및 Signaling에 필요한 Task Event 사용
실행
-
더보기
Launch( UE_SOURCE_LOCATION, []{ UE_LOG(LogTemp, Log, TEXT("Hello Tasks!")); } );
- UE_SOURCE_LOCATION
- Debug Name
- Task의 Debug 및 Task를 실행한 코드 탐색을 지원하는 용도
- 가급적 고유한 이름이 좋다.
- 이 매크로는 Source File의 Format File Name과 File이 사용되는 행에 String을 생성하는 매크로
- Debug Name
- Lambda 문
- 실제로 실행되는 Task Body Object
- UE_SOURCE_LOCATION
- Task 완료를 대기하거나, 그 결과를 받아야 하는 경우에는 Launch의 반환값을 사용한다.
-
더보기
FTask Task = Launch(UE_SOURCE_LOCATION, []{});
-
- 또한 Task 실행 결과를 반환할 수 있다.
-
더보기
TTask<bool> Task = Launch(UE_SOURCE_LOCATION, []{ return true; });
- 위 코드의 FTask도 사실 TTask<void>를 재정의 한 것이다.
-
- Task는 Async하게 실행이 되며, 실행 Thread와 동시에 실행될 수 있다.
- 때문에 순서는 따로 정의하지 않는다.
- 하지만 Task Priority를 지정해 순서에 영향을 미칠 수 있다.
-
더보기
Launch(UE_SOURCE_LOCATION, []{}, ETaskPriority::High);
- 기본값은 Normal이다.
-
- 보통은 Task Body로 Lambda를 사용하지만, 호출 가능한 Object를 사용할 수도 있다.
-
더보기
void Func() {} Launch(UE_SOURCE_LOCATION, &Func); struct FFunctor { void operator()() {} }; Launch(UE_SOURCE_LOCATION, FFunctor{});
-
디테일
- FTask는 Smart Pointer처럼 실제 Task의 Handle 역할을 한다.
- Task의 수명 역시 Reference Count로 관리한다.
- Task를 실행하면 수명과 필수 Resource를 할당한다.
- Reference를 초기화 하려면 다음과 같은 방식으로 Task Handle을 초기화 해야 한다.
-
더보기
FTask Task = Launch(UE_SOURCE_LOCATION, []{}); Task = {}; // release the reference
- Task Handle을 해제해도 Task가 바로 소멸되지 않는다.
- System이 Task 실행에 필요한 자체 Reference를 가지고 있기 때문이다.
- 이 Reference는 Task 완료 후에 해제가 된다.
완료 대기
Taks 완료 여부 확인
-
더보기
bool bCompleted = Task.IsCompleted();
Task 완료 대기
-
더보기
Task.Wait();
Timeout 있는 Task 완료 대기
-
더보기
bool bTaskCompleted = Task.Wait(FTimespan::FromMillisecond(100));
모든 Task 완료 대기
-
더보기
TArray<FTask> Tasks = {Task1, Task2}; Wait(Tasks);
Task 실행 결과 획득
-
더보기
TTask<int> Task = Launch (UE_SOURCE_LOCATION, []{ return 42; }); int Result = Task.GetResult();
- Launch는 Task 완료 및 결과가 준비될 때까지 Block된다.
Busy-Waiting
- Task는 실행 중 현재 Thread를 Block하기 때문에 완료가 되는 것을 기다리는 것은 비효율적이다.
- Busy-Waiting은 그 대안으로 제시가 된 기능이다.
- Busy-Waiting를 사용한 경우, Thread는 Task를 대기 중일 때 다른 작업을 실행하려 시도한다.
- 통제된 환경에서 사용하면 매우 유용하지만, 자체적으로 제한사항이 있어 신중하게 사용해야 한다.
- Busy-Waiting 도중에는 실행중인 Scheduler가 Task를 선택/제어 할 수 없다.
- 이러한 제한사항 때문에 다음 문제가 발생하기도 한다.
- Thread가 Busy-Waiting 상태에 진입해 재진입 가능성이 없는 Mutex를 Lock한 동안,
Scheduler가 Task를 선택해 동일한 Mutex를 Lock 하려 시도하는 Deadlock - 필수 경로에서 상대적으로 짧은 Task 때문에 Busy-Waiting을 수행하는 동안
Scheduler가 더 오래 걸리는 Task를 선택해 발생하는 성능 저하
- Thread가 Busy-Waiting 상태에 진입해 재진입 가능성이 없는 Mutex를 Lock한 동안,
Prerequisite, Subsequent
- Task는 다른 Task에 Dependency를 가질 수 있다.
- 이 때 Dependency의 대상이 되는 Task를 선행 Task(Prerequisite)
- Dependency를 가지는 Task를 후행 Task(Subsequent)라 지칭한다.
- Prerequisite를 설정하면 여러 Task를 대상으로 DAG를 만들 수 있다.
- 또한 Task Dependency에는 Worker Thread를 Block하지 않는다는 큰 이점이 있다.
- Task 실행 순서를 항상 변경할 수도 있다.
-
더보기
FTask Prerequisite = Launch(UE_SOURCE_LOCATION, []{}); FTask Subsequent = Launch(UE_SOURCE_LOCATION, []{}, Prerequisite);
FTask A = Launch(UE_SOURCE_LOCATION, []{}); FTask B = Launch(UE_SOURCE_LOCATION, []{}, A); FTask C = Launch(UE_SOURCE_LOCATION, []{}, A); FTask D = Launch(UE_SOURCE_LOCATION, []{}, Prerequisites(B, C));
중첩
- Task Dependency가 실행에 대한 Dependency라면, Nested Task는 완료에 대한 Dependency이다.
- Task A가 실행 도중 Task B를 실행한다 가정했을 때,
Task A는 자체 실행이 되더라도 Task B가 완료되어야 실행이 완료될 수 있다.
- Task A가 실행 도중 Task B를 실행한다 가정했을 때,
- Nested Task는 Task 기반 Async Interface를 노출할 때 흔히 볼 수 있는 패턴이다.
- 하지만 Task B는 내부 일부 구현을 누출하는 것이므로 바람직하지 않다.
-
더보기
FTask TaskA = Launch(UE_SOURCE_LOCATION, [] { FTask TaskB = Launch(UE_SOURCE_LOCATION, [] {}); AddNested(TaskB); } ); TaskA.Wait(); // `TaskA` 와 `TaskB` 가 전부 완료되어야 반환
- AddNested는 주어진 Task를 현재 Thread에 의해 실행되는 Task에 중첩됨을 표시한다.
- Task 내부에서 호출되지 않는 경우 Assert를 호출한다.
Pipe
- 동시 실행이 아닌, 연속적으로 실행되는 Task로 이루어진 Chain
- 기존의 공유 Resource에 여러 Thread가 Access 할 때에는 Synchronize를 위해 Mutex를 잠과 Lock을 건다.
- 다만 이 방법은 Thread가 차단되어 성능이 크게 저하되는 경우가 있다.
- 특히, Resource가 충돌 할 때에 저하가 심해진다.
- 복잡한 Resource를 사용 하는 경우, 다음 기능을 제공하는 편이 바람직하다.
- Async 작업을 시작할 수 있는 Interface
- 작업의 완료 여부를 확인할 수 있는 기능
- 완료 Event에 Delegate 하는 기능
- Pipe는 이 중 Async Interface의 구현을 간소화하기 위해 제공되었다.
- 공유 된 Resource마다 Pipe를 두고, 모든 Access를 Pipe에서 실행한 Task 내부에서 수행한다.
-
더보기
class FThreadSafeResource { public: TTask<bool> Access() { return Pipe.Launch(TEXT("Access()"), [this] { return ThreadUnsafeResource.Access(); }); } FTask Mutate() { return Pipe.Launch(TEXT("Mutate()"), [this] { ThreadUnsafeResource.Mutate(); }); } private: FPipe Pipe{ TEXT("FThreadSafeResource pipe")}; FThreadUnsafeResource ThreadUnsafeResource; }; FThreadSafeResource ThreadSafeResource; //여러 스레드에서 동일한 인스턴스에 동시 액세스함 bool bRes = ThreadSafeResource.Access().GetResult(); FTask Task = ThreadSafeResource.Mutate();
- FThreadSafeResource
- 이 Class로 Task에 따라 Public Thread Safe Async Interface를 추가할 수 있다.
- 이 Interface는 thread Unsafe REsource를 캡슐화 한다.
- FThreadSafeResource
- Pipe Task는 순차적 실행을 보장하기 때문에 추가 동기화는 필요 없다.
- 또한 가벼운 Object라 Task의 Collection을 저장하지 않는다.
- 심각한 Performance 저하 없이 수 천개의 Pipe를 사용할 수도 있다.
-
더보기
FPipe Pipe{ UE_SOURCE_LOCATION }; FTask TaskA = Pipe.Launch(UE_SOURCE_LOCATION, []{}); FTask TaskB = Pipe.Launch(UE_SOURCE_LOCATION, []{});
- 위에서 TaskA와 TaskB는 동시에 실행되지 않기에 서로 동기화 될 필요가 없다.
- 다만 실행 순서가 예상과 다를 수 있다.
- 대부분은 사실 예측이 가능하다.
-
- Pipe Task는 다른 Task와 동일한 기능을 지원한다.
- Dependency를 가진다거나
- Behaviour 순서를 따른다거나.
- 이 때 Pipe는 Dependency를 먼저 해결하고 다음 Task를 Pipe화 한다.
- 즉, Prerequisite를 가진 Task는 Pipe 실행을 차단하지 않는다.
- 또한, Prerequisite는 Pipe Task의 실행 순서를 변경할 수 있다.
- Pipe는 녹색 Thread로 취급한다.
- 녹색 Thread는 Worker Thread에서 실행되며, 쓰레드를 건너띌 수 있다.
- 위 예시로 설명하면, TaskA와 TaskB는 서로 다른 Thread에서 실행할 수 있다.
- 마지막으로 Pipe는 다음 성격도 가진다.
- Pipe API는 Thread Safe하다.
- Pipe Object는 Copy/Move가 불가하다.
- 한 Task를 여러 Pipe에서 실행할 수 없다.
Event
- Task Body가 없고 실행할 수 없는 특별한 Task Type
- 다른 Task와 달리, 초기에 실행되지 않으며 명시적으로 Trigger 해야만 한다.
- Synchronize 및 Primitive로 유용하다.
- 일회성 FEvent와 유사하게 사용된다.
- 다른 Task의 Prerequisite, Subsequent로 사용할 수 있다.
Task를 실행하되, 명시적으로 해제할 때까지 실행 보류
-
더보기
FTaskEvent Event{ UE_SOURCE_LOCATION }; FTask Task = Launch(UE_SOURCE_LOCATION, []{}, Event); Event.Trigger();
- Task Event를 다른 Task의 Prerequisite로 사용.
- 처음에는 Event가 Signaling 상태가 아니므로 완료되지 않는다.
- Task 역시 보류중인 Prerequiste가 있기 때문에, 이 Task(Event)가 해결되기 전까지 예약 및 실행되지 않는다.
- Task Event를 Trigger하면 Signaling 상태로 전환된다.
Task Event를 Jointer Task로 활용
-
더보기
FTask TaskA = Launch(UE_SOURCE_LOCATION, []{}); FTask TaskB = Launch(UE_SOURCE_LOCATION, []{}); FTaskEvent Joiner{ UE_SOURCE_LOCATION }; Joiner.AddPrerequisites(Prerequisites(TaskA, TaskB)); Joiner.Trigger(); ... Joiner.Wait();
- Joiner는 속해 있는 모든 Task에 대해 Dependnecy를 가진다.
- 즉, TaskA와 TaskB가 모두 끝나야 한다.
Task 실행 도중에 이벤트 발생을 대기
-
더보기
FTaskEvent Event{ UE_SOURCE_LOCATION }; FTask Task = Launch(UE_SOURCE_LOCATION, [&Event] { ... Event.Wait(); ... }); ... Event.Trigger();
- 일반적으로는 좋은 구조는 아니다.
- 가급적 Prerequisite를 다시 제작하는 것을 권장
Task를 실행하되, 자동 완료 Flag를 설정하지 않고 필요할 때 명시적으로 완료 처리
-
더보기
FTaskEvent Event{ UE_SOURCE_LOCATION }; FTask Task = Launch(UE_SOURCE_LOCATION, [&Event] { AddNested(Event); }); ... Event.Trigger();
Debug 및 Profiling
- 모든 Task, Task Event, Pipe에는 사용자가 만든 Debug Name이 있다.
- 이를 통해 Debuger가 Runtime중에 Task를 구별할 수 있다.
- 내부 상태를 검사하기 위해 Visual Studio Native Visualizer Tool이 제공된다.
- Unreal Insights에서는 Task 및 Task Event를 실행/예약/수행/완료 할 때 Visualize 할 Task Trace Channel을 추가한다.
'UE5 > Architecture' 카테고리의 다른 글
[Architecture] Command-line Arguments (0) | 2024.05.07 |
---|---|
[Architecture] Config File (0) | 2024.05.06 |
[Architecture] Programming Subsystem (0) | 2024.05.06 |
[Architecture] String (0) | 2024.05.06 |
[Architecture] Asset Registry (1) | 2024.05.06 |