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#과 동일합니다. async
와 await
를 사용하면 되는데요.
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()");
}
순서 흐름을 간단하게 정리한 것임으로 실제 처리 방식과는 다를 수 있습니다.
- 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되어도 비동기 처리는 계속 진행되고 있습니다.
그래서 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 반복문이 더 이상 진행되지 않는 것을 확인할 수 있습니다.
무엇을 선택해야 하나?
위에서 코루틴과 Async 차이를 살펴보았습니다. 이제 둘 중 무엇을 선택해야 할지 고민해야 할 차례인데요.
아시다시피 비동기 방식은 언제 어떻게 문제가 발생할지 파악하기 어려울 뿐더러 Critical section(임계 구역)
을 동시에 접근하지 못하도록 하는 처리 등 설계 시 고민해야 할 내용들이 많습니다.
무엇을 선택해야 할지 답이 정해져 있는 것은 아니지만 저는 이렇게 선택할 것 같습니다.
- 간단한 처리: 코루틴(게임 객체 움직임, UI 연출 etc)
- 게임 수명과 동일하게 살아있으면서 처리할 데이터가 많은 경우: Async (Ex. DB, 씬 배치, 서버 처리 etc)