Home [C#, Unity] 코루틴 대신 Async 사용하기
Post
Cancel

[C#, Unity] 코루틴 대신 Async 사용하기

1
2
Async를 공부하면서 작성한 글입니다.
틀린 부분이나 좋은 의견이 있다면 언제든지 댓글로 공유 부탁드립니다 :)

알다시피 Unity에서 사용하는 코드는 대부분 동기적으로 처리되고 있습니다.
그 중 비동기 처리가 필요할 때 대부분 코루틴을 사용하게 되는데요. 하지만 코루틴은 비동기처럼 동작하는 것 뿐이지 실제로는 동기적으로 처리되는 문법입니다.
그래서 코루틴에서 yield전에 처리할 양이 많으면 처리가 완료되는 동안 게임이 멈추게 되고 이를 해결하기 위해서는 비동기 처리가 가능한 Async를 사용해야 합니다.

그러므로 이 글에서 코루틴과 Async 차이를 하나씩 살펴보고 둘 중 무엇을 선택해야 할지 그리고 Async를 올바르게 사용하는 방법이 무엇인지 알아보도록 하겠습니다.

Unity에서 코루틴과 Async 차이

Async는 C#에서 사용하는 비동기 문법을 의미합니다.(비동기 프로그래밍 - C#)
Async의 이론이 궁금하시다면 공식 문서를 참고해주세요!

코루틴과 Async 차이는 많겠지만 크게 보면 2가지가 있다고 생각하는데 다음과 같습니다.

분류작동 방식값 반환 여부
코루틴동기적으로 작동반환 불가
Async비동기적으로 작동반환 가능

코루틴

작동 방식

코루틴은 동기적으로 작동하고 있습니다.
실제로 그렇게 작동하는지 테스트해본 내용은 [Unity] Coroutine은 비동기가 아니다 확인하실 수 있습니다.

함수 값 반환

코루틴은 값 반환이 불가합니다. 함수 반환 형식이 IEnumerator이기 때문인데요.

1
2
3
4
5
6
7
8
9
void test()
{
    StartCoroutine(CoroutineTest());
}

IEnumerator CoroutineTest()
{

}

그렇기 때문에 반환 값을 얻기 위해서는 파라미터를 이용해야 합니다.

1
2
3
4
5
6
7
8
9
10
void test()
{
    int result = 0;
    StartCoroutine(CoroutineTest(result));
}

IEnumerator CoroutineTest(int result)
{

}

Async

작동 방식

Async는 C#에서 지원하는 비동기 프로그래밍 문법이기 때문에 비동기적으로 작동됨을 알 수 있습니다.

함수 값 반환

Async는 코루틴과 다르게 아래 예제처럼 함수 반환이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
async void Test()
{
    Task<int> task = DoWorkAsync();
    int result = await task;
    Debug.Log(result);
}

// 값을 반환
async Task<int> DoWorkAsync()
{
    await Task.Delay(1000);
    return 100;
}

이로써 코루틴과 Async 차이를 확인해보았는데요.
추가로 Unity에서 Async를 어떻게 사용해야 하는지도 살펴보도록 하겠습니다.

Unity에서 Async 사용하기

Unity에서 Async를 사용하는 방법은 C#과 동일합니다. asyncawait를 사용하면 되는데요.
Task도 사용하기 위해서는 using System.Threading.Tasks;를 선언해주어야 합니다.

async와 await에 익숙하지 않으신 분들을 위해 간단히 설명하자면
async는 “이 함수가 비동기 처리 관련 기능을 쓸 수 있는 함수이다“라고 설명하는 키워드라고 보시면 됩니다.

그리고 async가 있는 함수에서 Task를 통해 쓰레드를 생성해서 비동기 처리를 지시할 수 있습니다.
그리고 await를 통해서 Task가 종료될 때까지 기다리는 처리를 할 수 있습니다.

위 내용을 코드로 보자면 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async void Test()
{
    Debug.Log("Start test()");
    await DoWorkAsync();
    Debug.Log("End test()");
}

async Task DoWorkAsync()
{
    Debug.Log("Start DoWorkAsync()");
    await Task.Delay(5000);

    Debug.Log("End DoWorkAsync()");
}

Async Test
실행 순서를 정리해보면 아래와 같습니다.

순서 흐름을 간단하게 정리한 것임으로 실제 처리 방식과는 다를 수 있습니다.

  • Test() 함수 실행
  • Start test() 로그 발생
  • DoWorkAsync() 함수 실행(await로 인해 종료 때까지 대기)
  • Start DoWorkAsync() 로그 발생
  • await Task.Delay(5000)를 통해 5초 대기
  • (5초 이후) End DoWorkAsync() 로그 발생
  • End test() 로그 발생

주의점

Unity에서 Async를 사용할 때 주의해야 할 점이 있습니다.
그것은 바로 스크립트가 붙어있는 객체가 Destroy 되거나 게임이 종료되어도 비동기 처리는 계속 진행된다는 점입니다.

예를 들어, 아래와 같이 코드를 구성하고 유니티 에디터를 실행하면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Start is called before the first frame update
void Start()
{
    Test();
    Destroy(this.gameObject);
}

async void Test()
{
    for (int i = 0; i < 100; i++)
    {
        Debug.Log(i);
        await Task.Delay(1000);
    }
}

아래 이미지처럼 객체가 Destroy되어도 비동기 처리는 계속 진행되고 있습니다.
Async Test2

그래서 CancellationToken를 사용해서 객체가 비활성화 또는 Destroy될 때 Task도 종료시켜주어야 합니다.

CancellationToken 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AsyncTest : MonoBehaviour
{
    CancellationTokenSource _cancellationTokenSource = new();


    // Start is called before the first frame update
    void Awake()
    {   
        // Task.Run 때 cancellationToken을 넘겨준다.
        Task.Run(Test, _cancellationTokenSource.Token);
        Destroy(this.gameObject);
    }

    private void OnDisable()
    {
        if(_cancellationTokenSource != null &&
            _cancellationTokenSource.Token.CanBeCanceled)
        {
            Debug.Log("Async is Cacnel");
            _cancellationTokenSource.Cancel();
        }
    }

    async void Test()
    {
        for (int i = 0; i < 100; i++)
        {
            if (_cancellationTokenSource.IsCancellationRequested)
                break;

            Debug.Log(i);
            await Task.Delay(1000);
        }
    }
}

위 코드 결과, 객체가 삭제되어 for 반복문이 더 이상 진행되지 않는 것을 확인할 수 있습니다.
CancellationTokenSource

무엇을 선택해야 하나?

위에서 코루틴과 Async 차이를 살펴보았습니다. 이제 둘 중 무엇을 선택해야 할지 고민해야 할 차례인데요.

아시다시피 비동기 방식은 언제 어떻게 문제가 발생할지 파악하기 어려울 뿐더러 Critical section(임계 구역)을 동시에 접근하지 못하도록 하는 처리 등 설계 시 고민해야 할 내용들이 많습니다.

무엇을 선택해야 할지 답이 정해져 있는 것은 아니지만 저는 이렇게 선택할 것 같습니다.

  • 간단한 처리: 코루틴(게임 객체 움직임, UI 연출 etc)
  • 게임 수명과 동일하게 살아있으면서 처리할 데이터가 많은 경우: Async (Ex. DB, 씬 배치, 서버 처리 etc)
This post is licensed under CC BY 4.0 by the author.