Unity 에서 Custom Awaitable 객체 만들기 메모

2025. 7. 17. 01:22Unity, C# 프로그래밍

키워드

 

Task (대기 가능한 객체)

- 내가 만든 MyTask 라는 클래스가 있을 때, MyTask를 await 하려면 MyTask 클래스가 MyTaskAwaiter(가명)를 반환하는 GetAwaiter() 를 구현해야 함.

- MyTask를 async 메서드의 반환형으로 사용하고 싶다면 AsyncMethodBuilder 지원이 필요함.


TaskAwaiter (대기자)

- ICriticalNotifyCompletion 인터페이스를 구현해야 함.

- 결과를 반환하는 GetResult() 함수를 구현해야 함.

- 작업이 완료되었는지 알 수 있는 IsCompleted 프로퍼티를 구현해야 함.


ICriticalNofifyCompletion

- TaskAwaiter 구현 시 사용 작업이 완료된 후 실행될 콜백 예약에 대해 구현.

- 기본적으로 OnCompleted() 및 UnsafeOnCompleted() 에서 넘어오는 continuation 델리게이트를 비동기 작업이 완료되었을 때 호출하도록 해 주면 됨.

- 만약 SynchronizationContext 및 TaskScheduler 지원이 필요하다면 OnCompleted() 및 UnsafeOnCompleted()에서 작업을 연계해 주어야 함.

- 여기서 구현하는 OnCompleted 및 UnsafeOnCompleted 는 AsyncMethodBuilder의 구현체에서 사용하게 됨.


AsyncMethodBuilder

- 특정 Task 타입에 대한 async/await 상태 머신 구현 지원.

- 내가 구현한 MyTask 가 async 함수의 반환형으로 사용될 경우 구현해야 함.


async / await

- await 구문은 GetAwaiter() 를 구현하는 클래스만이 가능함.

- async 메서드의 반환형으로 사용될 객체는 AsyncMethodBuilder 지원이 필요함.

  (ex. public async MyTask<T> Method() 라는 함수가 있다면 이때 MyTask<T> 객체가 AsyncMethodBuilder 지원 필요)


SynchronizationContext / TaskScheduler

- 작업 continue시 사용됨.

- 하나의 작업이 끝나고 다음 작업을 연계할 때 어느 위치에서 작업이 실행될 지 결정 (스레드 풀로 넘기거나 등등)

 





- System.Threading.Tasks.Task 및 ValueTask 는 위의 구현이 모두 되어있음.


- System.Threading.Tasks.Task 객체에 있는 ConfigureAwait(false) 사용 시 Awaiter를 ConfiguredTaskAwaitable 로 사용하게 되고, SynchronizationContext 및 TaskScheduler 를 무시하고 작업을 예약하게 된다.


- 유니티는 메인 스레드의 SynchronizationContext를 UnitySynchronizationContext로 재정의해서 여기로 예약되는 모든 continuation들을 메인스레드에서 폴링한다.
따라서 await 를 사용해도 이후 코드들이 다시 메인 스레드에서 실행되게 된다.


- 유니티에서 async 함수를 쓰고, 그 함수 내에서 작업 await시 이후 작업이 UnitySynchronizationContext에 의해서 메인 스레드에 예약되는데,
해당 Task를 동기 메서드인 Result 나 Wait() 사용 시 메인 스레드가 블로킹 되며 예약된 작업이 실행되지 못해 데드락이 걸릴 수 있다.
이걸 방지하려면 아예 작업 시작을 Task.Run() 으로 하거나 ConfigureAwait(false)로 사용하면 된다.

 

- 위의 코드들은 디컴파일러로 확인 시 실제로 어떻게 적용되는지 확인 가능.