[TIL] 비동기 프로그래밍(C Sharp)

muchan
muchan
∙∙조회 7

개요

그동안 Unity에서 비동기 메서드를 코루틴으로 구현했지만, UniTask 라이브러리를 통해 다른 방식으로 비동기 메서드를 작성해보고자 한다

스레드 기본 개념

스레드란?

  • 프로그램 내에서 코드를 실행하는 독립적인 실행 흐름
  • cpu가 동시에 여러 작업을 처리하는 것처럼 보이게 만드는 단위

Unity 엔진은 메인 스레드에서 게임 로직을 실행한다

Unity API와 스레드 제약

Unity의 거의 모든 API는 메인 스레드에서만 호출 가능

csharp
// ✅ 메인 스레드에서 실행 - 정상 동작
void Update()
{
    transform.position = Vector3.zero;
    GameObject obj = Instantiate(prefab);
    Debug.Log("Hello");
}

// ❌ 다른 스레드에서 실행 - 예외 발생
void SomeBackgroundThread()
{
    transform.position = Vector3.zero; // 예외!
    GameObject obj = Instantiate(prefab); // 예외!
}

백그라운드 스레드

무거운 작업은 별도의 스레드에서 실행해야 메인 스레드가 멈추지 않는다

주의: 백그라운드 스레드에서 Unity API를 사용할 수 없다(Transform, Rigidbody 등)

C Sharp 비동기 프로그래밍

목표: 코루틴으로 처리하던 코드들을 C# 표준 비동기(async, await, Task)로 작성하기

async/await가 해결하는 문제

  • 메인 스레드 블로킹 x
  • 자동으로 메인 스레드로 복귀
  • 복잡한 스레드 관리 없이 간단한 코드 작성
  • Unity API를 안전하게 사용

코루틴과의 비교

구분코루틴async/await
실행 주체Unity 엔진.NET 런타임
반환 타입IEnumeratorTask, Task
재개 시점 제어yield returnawait
예외 처리어려움try-catch 사용 가능
취소StopCoroutineCancellationToken

코루틴 vs async/await

csharp
IEnumerator MyCoroutine()
{
    transform.position = Vector3.zero; // ✅ 가능
    yield return new WaitForSeconds(1);
    transform.position = Vector3.one;  // ✅ 가능 (항상 메인 스레드)
}

코루틴의 핵심: Unity가 매 프레임 메인 스레드에서 MoveNext() 호출 → 항상 Unity API 사용 가능

async/await도 비슷하게 동작

csharp
async Task MyAsyncMethod()
{
    transform.position = Vector3.zero;  // ✅ 가능
    await Task.Delay(1000);
    transform.position = Vector3.one;   // ✅ 가능 (자동으로 메인 스레드 복귀)
}

Task.Run

: 백그라운드 스레드 풀에서 작업 실행

  • CPU 집약적 계산에 적합
  • Unity API 절대 사용 금지
csharp
async Task CorrectUsage()
{
    // 1. 백그라운드에서 계산만
    int result = await Task.Run(() => {
        int sum = 0;
        for (int i = 0; i < 1000000; i++)
            sum += i;
        return sum; // Unity API 안 씀 ✅
    });

    // 2. await 이후 자동으로 메인 스레드로 복귀
    Debug.Log($"결과: {result}"); // ✅ 안전
    transform.position = new Vector3(result, 0, 0); // ✅ 안전
}

Task란?

  • 비동기 작업의 실행 상태를 나타내는 객체
  • "미래에 완료될 작업"을 표현하는 약속

Task의 생명주기 (상태)

async 메서드가 반환하는 Task는 WaitingForActivation에서 시작한다:

  • WaitingForActivation: 비동기 작업 진행 중 (async 메서드 Task의 시작 상태)
  • Running: 작업 실행 중
  • RanToCompletion: 성공적으로 완료
  • Faulted: 예외 발생으로 실패
  • Canceled: 취소됨

Created 상태는 new Task()로 직접 만든 cold task 전용이라, async/await 코드에서는 보이지 않는다.

async/await 패턴과 동작 원리

async 키워드

  • 메서드를 비동기 메서드로 표시
  • 메서드 내에서 await 사용 가능하게 함
  • 반환 타입: Task, Task , void(이벤트 핸들러만), ValueTask, ValueTask 

await 키워드

  • Task가 완료될 때까지 비동기적으로 대기
  • 메서드 실행을 일시 중단하고 호출자에게 제어 반환
  • Task 완료 시 메서드 재개

실행 흐름 예시

csharp
async Task ExampleFlow()
{
    Debug.Log("1. 시작");

    await Task.Delay(1000); // 여기서 메서드 일시 중단

    Debug.Log("2. 1초 후"); // Task 완료 후 재개
}

실제 실행 순서:

  1. “1. 시작” 출력
  2. Task.Delay(1000) 시작, 메서드는 즉시 반환 (호출자로 제어 이동)
  3. 1초 후 Task 완료, 메서드 재개
  4. “2. 1초 후” 출력