unitask를 알아보기 전 코루틴에 대해서 먼저 가볍게 알아보겠다.
코루틴이란?
유니티에서 사용되는 코루틴은 비동기 프로그래밍을 수행하기 위한 기능 중 하나이다.
코루틴은 일시 중지할 수 있는 함수의 실행을 가능하게 하며, 특정 조건이 충족될 때 까지 기다린 후에 실행을 계속할 수 있도록 한다. 코루틴의 기본원칙은 ' Enumerator ' 인터페이스를 반환하는 함수를 통해 작성된다.
UniTask란?
유니티에서 unitask는 비동기 작업을 처리하기 위한 도구로, 유니티에 내장되어 있는 코루틴 기능을 대체하며 async/await 패턴을 지원한다.
일반적인 C#의 Task와 비교하여, Unitask는 유니티에서 더 효율적으로 동작하도록 설계되어있고, 주로 WebGL 빌드와 같은 플랫폼에서 기본 Task의 제한을 극복하기 위해 사용된다.
Unitask 다운 경로
아래 깃허브 링크에서 다운받을 수 있다.
GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.
Provides an efficient allocation free async/await integration for Unity. - Cysharp/UniTask
github.com
그럼 코루틴 과 unitask의 장단점을 알아보겠다.
- 코루틴의 장점은 추가 라이브러리가 불필요하며, 시간 제어 가능 ( yield return ) 하지만 IEnumerrator 반환 함수에 제한 그리고 복잡한 비동기 작업 처리 어려움 예) 'WhenAll' 메서드를 직접 구현 과 같은 단점이 있다.
- unitask의 장점은 async/await 구문을 활용한 비동기 프로그래밍과 WhenAll, WhenAny 등의 내장 메서드 제공, Zero Allocation 기능으로 성능향상이 있다. 하지만 추가 라이브러리를 다운받아야한다는 단점이 있다.
Zero Allocation (할당 없음)
- unitask는 zero allocation 기능을 제공하여 메모리 할당을 최소화 한다. 불필요한 가비지 컬렉션 (GC)을 방지하여 프레임 드랍이나 렉을 줄일 수 있다.- 코루틴은 메모리 할당을 필요로 하며, 이는 가비지 컬렉션 오버헤드를 발생시킬 수 있다.
public class CoroutineExample : MonoBehaviour
{
private void Start()
{
StartCoroutine(MyCoroutine());
}
private IEnumerator MyCoroutine()
{
while (true)
{
yield return new WaitForSeconds(1f); // 메모리 할당 발생!
Debug.Log("Coroutine running every 1 second");
}
}
}
위 코드는 코루틴으로 작성한 예시이며, 1초마다 메모리 할당이 발생한다.
public class UniTaskExample : MonoBehaviour
{
private async void Start()
{
await MyUniTask();
}
private async UniTask MyUniTask()
{
while (true)
{
await UniTask.Delay(1000); // Zero Allocation
Debug.Log("UniTask running every 1 second");
}
}
}
위 코드는 unitask로 작성한 예시이며, 1초마다 메모리 할당이 발생하지 않는다.
WhenAll이란?
WhenAll은 여러 비동기 작업이 모두 완료될 때까지 기다리는데 사용되는 메서드!"UniTask"의 WhenAll은 내장 메서드로 제공되어 사용이 간편하지만, "코루틴" 에서는 이러한 메서드를 직접 구현해야함!
public class CoroutineWhenAllExample : MonoBehaviour
{
private void Start()
{
StartCoroutine(WaitForAll());
}
private IEnumerator WaitForAll()
{
Coroutine task1 = StartCoroutine(Task(2)); // 2초 대기
Coroutine task2 = StartCoroutine(Task(3)); // 3초 대기
yield return task1;
yield return task2;
Debug.Log("All tasks completed");
}
private IEnumerator Task(float seconds)
{
yield return new WaitForSeconds(seconds);
Debug.Log($"Task completed after {seconds} seconds");
}
}
위 코드는 코루틴으로 작성한 예시이다.
모든 Task가 완료될 때까지 기다리기 위해, 각각의 코루틴을 생성하고 완료를 기다리는 코드를 추가로 작성한 것이다.
public class UniTaskWhenAllExample : MonoBehaviour
{
private async void Start()
{
await UniTask.WhenAll(Task1(), Task2());
Debug.Log("All tasks completed");
}
private async UniTask Task1()
{
await UniTask.Delay(2000); // 2초 대기
Debug.Log("Task1 completed after 2 seconds");
}
private async UniTask Task2()
{
await UniTask.Delay(3000); // 3초 대기
Debug.Log("Task2 completed after 3 seconds");
}
}
위 코드는 UniTask로 작성한 예시이다.
WhenAll 메서드를 이용하여 Task1 메서드와 Task2 메서드를 기다리는 비동기 작업을 손쉽게 만들 수 있다.
UniTask 사용방법
private void Start()
{
StartCoroutine(CoroWait());
UniWait().Forget();
}
IEnumerator CoroWait()
{
yield return new WaitForSeconds(3f);
}
async UniTaskVoid UniWait()
{
await UniTask.Delay(TimeSpan.FromSeconds(3f));
}
await UniTask.Delay(TimeSpan.FromSeconds(3f), DelayType.UnscaledDeltaTime);
- DelayType.UnscaledDeltaTime을 사용하면 Time.ignore와 같은 상황을 무시할 수 있다.
WaitUntil
private int count = 0;
IEnumerator CoroWait()
{
yield return new WaitUntil(() => count == 7);
}
async UniTaskVoid UniWait()
{
await UniTask.WaitUntil(()=> count == 7);
}
특정 조건이 될 때까지 기다려야 하는 조건은 위와 같이 작성할 수 있습니다.
Task 중단, CanellationTokenSource
private CancellationTokenSource cancel = new CancellationTokenSource();
private void Update()
{
if (~~)
{
cancel.Cancel();
}
}
async UniTaskVoid UniWait()
{
await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: cancel.Token);
}
만약 특정 조건이 만졸될 때까지 실행되는 while문의 경우
CancellationToken이 Cancel 될 때 while 문을 종료해주는 로직을 따로 작성해주어야 한다.
private async UniTask WaitUntilInitializedAsync(CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
/~
~/
await UniTask.Delay(100, ignoreTimeScale:true, cancellationToken: cancellationToken);
}
}
isCancellationRequested는 CancellationToken이 취소 요청을 받았는지 여부를 나타낸다.
CancellationToken의 Cancel 메서드가 호출되면 IsCancellationRequested가 true로 설정된다.
오브젝트가 파괴될 때 UniTask 취소
var token = this.GetCancellationTokenOnDestroy();
await UniTask.Delay(1000, cancellationToken: token);
GetCancellationTokenOnDestroy 함수는 UniTask 라이브러리의 일부로 제공되며,
GameObject나 Component가 파괴될 때 발생하는 취소 토큰을 가져옵니다.
이 함수를 사용하면 GameObject나 Component가 파괴된 시점에 UniTask를 취소할 수 있습니다.
위 코드를 예시로 들면, UniTask.Delay는 1초 후에 완료되도록 예정되어 있지만,
해당 GameObject가 그전에 파괴되면 UniTask는 취소됩니다.
Frame 대기
// replacement of yield return null
await UniTask.Yield();
await UniTask.NextFrame();
// replacement of WaitForEndOfFrame(requires MonoBehaviour(CoroutineRunner))
await UniTask.WaitForEndOfFrame(this)
Update문과 같은 역할을 하는 UniTask
private async UniTaskVoid UpdateUniTask()
{
while (true)
{
///
~~
///
await UniTask.Yield(PlayerLoopTiming.Update);
}
}
이때, PlayerLoopTiming에는 Update 외에 "PreUpdate, Update, LateUpdate 등"의 열거형 키워드 존재한다.
Touple 값 받기
var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
//WhenAll은 task1, task2, task3 모두 완료될 때까지 기다린다는 뜻이다.
Timeout handling
"CancellationTokenSouce.CancelAfterSlim(TimeSpan)"
var cancel = new CancellationTokenSource();
cancel.CancelAfterSlim(TimeSpan.FromSeconds(5));
try
{
await UnityWebRequest.Get("http://~~").SendWebRequest().WithCancellation(cancel.Token);
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == cancel.Token)
{
Debug.Log("Timeout");
}
}
5초 기다리는 동안 Task를 성공적으로 마무리하지 못하면 예외처리 하는 로직이다.
Error-Handling
public class ErrorHandlingExample : MonoBehaviour
{
private async UniTaskVoid Start()
{
try
{
await UniTask.Run(() => ThrowException());
}
catch (System.Exception e)
{
Debug.LogError(e.Message);
}
}
private void ThrowException()
{
throw new System.Exception("An error occurred!");
}
}
예외를 던지는 메서드 ThrowException을 UniTask.Run 내부에 캡슐화합니다.
그런 다음 이 UniTask를 try-catch 블록으로 감쌉니다. 예외가 발생하면 캐치 블록에 의해 예외가 잡히고 오류 메시지가 콘솔에 기록할 수 있다.
별도의 스레드에서 비동기적으로 실행시키기
public class UniTaskRunExample : MonoBehaviour
{
private async UniTaskVoid Start()
{
int result = await UniTask.Run(() => Compute());
}
private int Compute()
{
int sum = 0;
for (int i = 0; i < 1000000; i++)
{
sum += i;
}
return sum;
}
}
UniTask.Run을 이용해서 동기식 코드를 스레드풀에서 비동기식으로 실행시킬 수 있다.
메인 스레드가 멈추는 것을 방지하기 위해 백그라운드 스레드에서 실행하려는 CPU 바운드 작업이 있을 때 유용하다.
UniTask와 UniTaskVoid 차이
두 타입의 주요 차이는 "반환 값의 유무"와 관련이 있다.
UniTask
결과를 반환하는 비동기 작업을 나타낸다.
UniTaskVoid
결과를 반환하지 않는 비동기 작업을 나타낸다.
UniTaskVoid는 호출한 후 다음 줄로 넘어가는 반면,
UniTask는 비동기 작업을 호출하고 비동기 작업이 끝날 때까지 기다립니다.
public async UniTaskVoid FireAndForget()
{
await UniTask.Delay(TimeSpan.FromSeconds(2));
Debug.Log("Fire and forget task completed");
}
public void StartOperation()
{
FireAndForget().Forget();
Debug.Log("Started fire and forget task, not waiting for it");
}
FireAndForget 비동기를 호출하고 나면,
"Started fire and forget task, not waiting for it" Log가 바로 출력됩니다.
public async UniTask Fire()
{
await UniTask.Delay(TimeSpan.FromSeconds(2));
Debug.Log("Fire and forget task completed");
}
public async void StartOperation()
{
await Fire();
Debug.Log("Started fire and forget task, not waiting for it");
}
Fire 비동기를 호출하고 2초 대기 후,
"Fire and forget task completed", "Started fire and forget task, not waiting for it" Log가 순차적으로 출력됩니다.
'유니티' 카테고리의 다른 글
유니티 - Mirror 셋팅 및 Attribute 간단 설명 (0) | 2024.06.09 |
---|---|
유니티 최적화 - Coroutine (0) | 2024.05.11 |
유니티 최적화 - Engine (0) | 2024.05.10 |
유니티 최적화 - Garbage Collection (0) | 2024.05.09 |
간단 Action / Func 예제 (0) | 2024.04.14 |