https://dev.epicgames.com/documentation/ko-kr/unreal-engine/tasks-systems-in-unreal-engine

 

언리얼 엔진의 태스크 시스템 | 언리얼 엔진 5.4 문서 | Epic Developer Community

언리얼 엔진의 태스크 시스템에 대한 개요입니다.

dev.epicgames.com

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/tasks-system-references-in-unreal-engine#busywait

 

언리얼 엔진의 태스크 시스템 레퍼런스 | 언리얼 엔진 5.4 문서 | Epic Developer Community

태스크 시스템에서 실행 가능한 여러 태스크에 대한 레퍼런스입니다.

dev.epicgames.com

  • 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을 생성하는 매크로
    • Lambda 문
      • 실제로 실행되는 Task Body Object
  • 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 완료 대기

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를 선택해 발생하는 성능 저하

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가 완료되어야 실행이 완료될 수 있다.
  • 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를 캡슐화 한다.
  • 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

+ Recent posts