[Unity/C#] UniTask 이해하기 Task, Coroutine과의 차이

2026. 1. 31. 22:51·Programming Language/C#

UniTask를 이해하기 전에 C#의 async / await, Task, ValueTask, CancellationToken의 이해가 선행되는게 좋다.

따라서 이 글에서는 위의 개념들을 간략하게만 짚고 넘어간다. 

 

 

배경

Unity는 기본적으로 코루틴(Coroutine)을 통해 비동기 처리를 제공한다. 다만 이는 아래와 같이 여러 문제점들이 존재한다.

1. 시작 시 MonoBehaviour와 결합해야 한다.

2. 콜백 프로세스로 반환 값과 예외를 전달하기 어렵다.

3. 람다와 코루틴 자체의 할당이 필요하다.

4. 취소 처리가 어렵다. 멈출 수는 있지만 사용한 자원 정리가 불가능하다. (finally, dispose 등)

5. 다중 코루틴을 직렬, 또는 병렬로 제어하기가 번거롭다.

 

C# 5.0부터 제공된 async / await로 인해 비동기 코드는 동기 코드와 거의 동일하게 작성할 수 있게 되었다.

절차적으로 작성할 수 있어 콜백 중첩으로 생기는 문제들이 사라지고, 반환 값과 예외 처리, 다중 비동기 처리를 자연스럽게 다룰 수 있다.

 

Unity는 최신 버전의 C#을 지원하므로 async / await을 사용할 수 있으나, 프레임워크 차원의 지원이 없어 그대로는 실용적으로 사용하기 어렵다.

또한 Task와 함께 사용 시 메모리 할당이 불가피하여 GC에게 리소스를 내어줄 수밖에 없다. (class인 Task 객체 할당, 상태 머신 박싱으로 인한 할당, MoveNext용 delegate 할당 등)

이를 해결하기 위해 ValueTask 타입이 도입되었으나, 비동기 작업이 필요한 경우에는 여전히 Task 객체 할당과 상태 머신 박싱이 존재한다.

 

 

UniTask

 

UniTask는 Cysharp 사에서 배포한 오픈소스 라이브러리이며, Unity에 특화된 비동기 프레임워크이다.

C# 7.0에 추가된 사용자 커스텀 AsyncMethodBuilder를 구현하여 만들어졌고, 동작 방식을 ValueTask / IValueTaskSource 와 유사하게 구현하여 표준 동작에 맞춰 학습 격차를 줄였다고 한다.

 

UniTask의 State는 아래와 같다.

- Pending (생성 ~ 완료 전)

- Succeeded (정상 반환)

- Faulted (예외 발생)

- Canceled (OperationCanceledException 발생)

Faulted와 Canceled는 상위 레벨로 예외가 전파된다.

 

Task와 마찬가지로 CancellationToken을 인자로 전달해서 취소할 수 있다.

MonoBehaviour의 OnDestory()에서 Cancel()과 Dispose()를 호출하면, 토큰이 전달된 모든 곳에서 누수 없이 작업을 취소할 수 있다.

UniTask.SuppressCancellationThrow()를 통해 비용이 큰 예외 전달을 피할 수도 있다.

 

Zero-Allocation

 

UniTask는 값 타입으로 즉시 값을 반환하는 경우 ValueTask처럼 할당을 하지 않는다.

 

await 키워드를 만나게 되면 다음 번 수행이 재개될 때까지 스택 메모리에 있던 상태 머신정보 보존을 위해 힙 메모리에 넣는데,

Task, ValueTask처럼 박싱을 하지 않고 AsyncUniTask<TStateMachine> 라는 class의 Runner 객체를 하나 생성해 상태 머신을 필드에 복사해 넣어 이동시킨다.

더 나아가 AsyncUniTask<TStateMachine> 은 pooling을 사용하여 GetResult()가 호출되면 pool로 반환되어 재사용된다.

이를 통해 Task 할당 및 상태 머신 박싱 관련 할당을 제거했다.

 

아래는 UniTask의 SetStateMachine() 부분 코드다.

 

// StateMachineRunner.cs
internal sealed class AsyncUniTask<TStateMachine> : IStateMachineRunnerPromise, IUniTaskSource, ITaskPoolNode<AsyncUniTask<TStateMachine>>
    where TStateMachine : IAsyncStateMachine
{
    static TaskPool<AsyncUniTask<TStateMachine>> pool;

	...

    public Action MoveNext { get; }

    TStateMachine stateMachine;

    AsyncUniTask() {
        MoveNext = Run;
    }

    public static void SetStateMachine(ref TStateMachine stateMachine, ref IStateMachineRunnerPromise runnerPromiseFieldRef) {
        if (!pool.TryPop(out var result)) {
            result = new AsyncUniTask<TStateMachine>();
        }
        TaskTracker.TrackActiveTask(result, 3);

        runnerPromiseFieldRef = result; // set runner before copied.
        result.stateMachine = stateMachine; // copy struct StateMachine(in release build).
    }

    ...
}

 

 

pooling을 사용하기 때문에 사용에 있어 제약 사항이 ValueTask과 동일하다.

1. 인스턴스를 여러 번 await 하는 행위

2. AsTask를 여러 번 호출하는 행위

3. 작업이 아직 완료되지 않았을 때 .GetAwaiter().GetResult()를 호출하거나, 이를 여러 번 호출하기

 

SynchronizationContext / ExecutionContext

 

Task에서 사용하는 SynchronizationContext와 ExecutionContext 캡쳐를 전혀 사용하지 않는다.

이는 실행하는 순간의 Context를 기억해두었다가, 나중에 기억해 둔 Context에서 실행하게 할 건지를 결정하는 부분이다.

 

이는 스택 트레이스가 복잡해지고 오버헤드를 유발하는데, 싱글 스레드인 Unity에서는 사용하지 않아 제거하여 성능을 향상시켰다.

Unity의 경우에는 자동으로 메인 스레드로 돌아오게 되는데, Unity의 비동기 처리 AsyncOperation은 엔진 계층(C++)에서 실행되고, 스크립트 계층(C#)에서는 이미 메인 스레드로 돌아와있다.

 

하지만 Unity의 경우에도 실행 시퀀스 호출을 세밀하게 제어해야 하는 상황이 많다.

예를 들어 코루틴에서 WaitForEndOfFrame이나 WaitForFixedUpdate 등을 사용하는 상황이다.

그래서 UniTask는 돌아올 실행 시퀀스 지점을 수동으로 지정할 수 있다.

모든 PlayerLoop 메커니즘의 각각 시작과 끝에 주입되어 총 14개 위치를 선택할 수 있다.

 

↓ 상세 위치 확인 (화살표로 표시)

더보기

Initialization

→ UniTaskLoopRunnerYieldInitialization
→ UniTaskLoopRunnerInitialization
PlayerUpdateTime
DirectorSampleTime
AsyncUploadTimeSlicedUpdate
SynchronizeInputs
SynchronizeState
XREarlyUpdate
→ UniTaskLoopRunnerLastYieldInitialization
→ UniTaskLoopRunnerLastInitialization

 

EarlyUpdate

→ UniTaskLoopRunnerYieldEarlyUpdate
→ UniTaskLoopRunnerEarlyUpdate
PollPlayerConnection
ProfilerStartFrame
GpuTimestamp
AnalyticsCoreStatsUpdate
UnityWebRequestUpdate
ExecuteMainThreadJobs
ProcessMouseInWindow
ClearIntermediateRenderers
ClearLines
PresentBeforeUpdate
ResetFrameStatsAfterPresent
UpdateAsyncReadbackManager
UpdateStreamingManager
UpdateTextureStreamingManager
UpdatePreloading
RendererNotifyInvisible
PlayerCleanupCachedData
UpdateMainGameViewRect
UpdateCanvasRectTransform
XRUpdate
UpdateInputManager
ProcessRemoteInput
ScriptRunDelayedStartupFrame
UpdateKinect
DeliverIosPlatformEvents
TangoUpdate
DispatchEventQueueEvents
PhysicsResetInterpolatedTransformPosition
SpriteAtlasManagerUpdate
PerformanceAnalyticsUpdate
→ UniTaskLoopRunnerLastYieldEarlyUpdate
→ UniTaskLoopRunnerLastEarlyUpdate

 

FixedUpdate

→ UniTaskLoopRunnerYieldFixedUpdate
→ UniTaskLoopRunnerFixedUpdate
ClearLines
NewInputFixedUpdate
DirectorFixedSampleTime
AudioFixedUpdate
ScriptRunBehaviourFixedUpdate
DirectorFixedUpdate
LegacyFixedAnimationUpdate
XRFixedUpdate
PhysicsFixedUpdate
Physics2DFixedUpdate
DirectorFixedUpdatePostPhysics
ScriptRunDelayedFixedFrameRate
→ UniTaskLoopRunnerLastYieldFixedUpdate
→ UniTaskLoopRunnerLastFixedUpdate

 

PreUpdate

→ UniTaskLoopRunnerYieldPreUpdate
→ UniTaskLoopRunnerPreUpdate
PhysicsUpdate
Physics2DUpdate
CheckTexFieldInput
IMGUISendQueuedEvents
NewInputUpdate
SendMouseEvents
AIUpdate
WindUpdate
UpdateVideo
→ UniTaskLoopRunnerLastYieldPreUpdate
→ UniTaskLoopRunnerLastPreUpdate

 

Update

→ UniTaskLoopRunnerYieldUpdate
→ UniTaskLoopRunnerUpdate
ScriptRunBehaviourUpdate
ScriptRunDelayedDynamicFrameRate
ScriptRunDelayedTasks
DirectorUpdate
→ UniTaskLoopRunnerLastYieldUpdate
→ UniTaskLoopRunnerLastUpdate

 

PreLateUpdate

→ UniTaskLoopRunnerYieldPreLateUpdate
→ UniTaskLoopRunnerPreLateUpdate
AIUpdatePostScript
DirectorUpdateAnimationBegin
LegacyAnimationUpdate
DirectorUpdateAnimationEnd
DirectorDeferredEvaluate
EndGraphicsJobsAfterScriptUpdate
ParticleSystemBeginUpdateAll
ConstraintManagerUpdate
ScriptRunBehaviourLateUpdate
→ UniTaskLoopRunnerLastYieldPreLateUpdate
→ UniTaskLoopRunnerLastPreLateUpdate

 

PostLateUpdate

→ UniTaskLoopRunnerYieldPostLateUpdate
→ UniTaskLoopRunnerPostLateUpdate
PlayerSendFrameStarted
DirectorLateUpdate
ScriptRunDelayedDynamicFrameRate
PhysicsSkinnedClothBeginUpdate
UpdateRectTransform
UpdateCanvasRectTransform
PlayerUpdateCanvases
UpdateAudio
VFXUpdate
ParticleSystemEndUpdateAll
EndGraphicsJobsAfterScriptLateUpdate
UpdateCustomRenderTextures
UpdateAllRenderers
EnlightenRuntimeUpdate
UpdateAllSkinnedMeshes
ProcessWebSendMessages
SortingGroupsUpdate
UpdateVideoTextures
UpdateVideo
DirectorRenderImage
PlayerEmitCanvasGeometry
PhysicsSkinnedClothFinishUpdate
FinishFrameRendering
BatchModeUpdate
PlayerSendFrameComplete
UpdateCaptureScreenshot
PresentAfterDraw
ClearImmediateRenderers
PlayerSendFramePostPresent
UpdateResolution
InputEndFrame
TriggerEndOfFrameCallbacks
GUIClearEvents
ShaderHandleErrors
ResetInputAxis
ThreadedLoadingDebug
ProfilerSynchronizeStats
MemoryFrameMaintenance
ExecuteGameCenterCallbacks
ProfilerEndFrame
→ UniTaskLoopRunnerLastYieldPostLateUpdate
→ UniTaskLoopRunnerLastPostLateUpdate

 

기타 확장, 유틸리티 지원

 

기본적으로 Unity의 모든 비동기 객체에 대한 await 지원이 모두 포함되어 있다.

(AsyncOperation, ResourceRequest, AssetBundleRequest, AssetBundleCreateRequest, UniTaskWebRequestAsyncOperation)

 

또한 v2부터 DOTween이나 Addressables 같은 외부 에셋에 대한 지원도 확장되었다.

 

직접 await하는 것 외에도 WithCancellation() 메서드를 호출하여 취소 지원을 받을 수 있다.

ToUniTask()는 진행률 콜백, 실행할 PlayerLoop, CancellationToken을 전달 할 수 있다.

 

기타 비동기 이벤트 핸들링 OnClickAsync, OnCollisionEnterAsync를 지원하며,

UniTaskAsyncEnumerable를 통한 비동기 LINQ도 지원한다. (C# 8.0)

 

UniTask Tracker라는 메모리 누수 추적을 위해 현재 실행 중인 UniTask 목록을 표시해주는 툴을 제공한다.

생성 시점의 스택 트레이스를 유지하므로, 어디서 생상된 UniTask가 새고 있는지 쉽게 찾을 수 있다.

 

 

참고 문헌

https://github.com/Cysharp/UniTask
https://github.com/Cysharp/UniTask/blob/master/src/UniTask/Assets/Plugins/UniTask/Runtime/CompilerServices/StateMachineRunner.cs
https://neuecc.medium.com/unitask-a-new-async-await-library-for-unity-a1ff0766029
https://neuecc.medium.com/unitask-v2-zero-allocation-async-await-for-unity-with-asynchronous-linq-1aa9c96aa7dd

 

개인 공부용 포스팅인 점을 참고하시고, 잘못된 부분이 있다면 댓글로 남겨주시면 감사드리겠습니다.

 

'Programming Language > C#' 카테고리의 다른 글

[C#] 이터레이터 이해하기 IEnumerable / IEnumerator  (0) 2026.02.15
[C#] 람다 표현식(Lambda Expression) 이해하기  (0) 2026.02.14
[C#] CancellationToken / CancellationTokenSource 작성 및 내부 구현  (1) 2026.01.11
[C#] ValueTask 이해하기, Task와의 차이점  (0) 2026.01.03
[C#] 비동기 코드 async / await / Task 작성 및 내부 구현  (0) 2025.12.29
'Programming Language/C#' 카테고리의 다른 글
  • [C#] 이터레이터 이해하기 IEnumerable / IEnumerator
  • [C#] 람다 표현식(Lambda Expression) 이해하기
  • [C#] CancellationToken / CancellationTokenSource 작성 및 내부 구현
  • [C#] ValueTask 이해하기, Task와의 차이점
SikPang
SikPang
게임 개발 공부 블로그
  • Second Step : 공부 블로그
    SikPang
    • 분류 전체보기 (46)
      • Low-level (20)
        • Computer System (11)
        • Operating System (1)
        • System Programming (8)
      • Data Structure (5)
      • Algorithm (3)
      • Computer Graphics (1)
      • Programming Language (10)
        • C# (10)
        • C++ (0)
      • Game Engine (5)
        • Unity (5)
        • Unreal (0)
      • Memoir (2)
      • Devlog (0)
  • 최근 글

  • 전체
    오늘
    어제


  • hELLO· Designed By정상우.v4.10.6
SikPang
[Unity/C#] UniTask 이해하기 Task, Coroutine과의 차이
상단으로

티스토리툴바