private void LoadMainMenu()
{ DownloadPlayerAssets(); // 5초 소요 - 게임 멈춤! DownloadWorldAssets(); // 8초 소요 - 여전히 멈춘 상태! DownloadAudioAssets(); // 3초 소요 - 여전히 멈춘 상태!
ShowMainUI(); // 게임이 16초 동안 멈춘 후}각 다운로드가 완료될 때까지 기다린 이후에 다음 다운로드로 넘어갑니다.
이로 인해 UI와 게임플레이가 16초 동안 완전히 멈추게 됩니다.기술적으로 각 작업을 더 작은 단계들로 나누거나 다른 스레드에서 실행할 수 있다고 하더라도,현재 구조에서는 모든 작업이 완료될 때까지 메인 스레드가 강제로 차단됩니다.

그림 1. 동기 실행 - 각 작업이 완료될 때까지 메인 스레드가 차단됩니다.
비동기 프로그래밍 - 메인 스레드 차단 없이 진행
위의 동기식 예시에서 메인 스레드는 각 작업이 완료될 때까지 기다린 후 다음 작업을 진행합니다.
하지만 작업 시작과 작업 완료 대기를 분리하면 어떻게 될까요?
각 작업의 시작이 이전 작업의 완료와 동기화될 필요가 없도록 코드를 작성하면 어떨까요?
private void LoadMainMenu()
{ StartDownloadPlayerAssets(); // 시간이 걸리지만 기다릴 필요는 없음 StartDownloadWorldAssets(); // 곧이어 시작됨 StartDownloadAudioAssets(); // 즉시 시작됨 ShowMainUI(); // UI가 즉시 나타날 수 있음}
이제는 각 작업이 완료될 때까지 기다리지 않고 다음으로 넘어가게 되었습니다.대신 각 StartDownloadXXX() 메서드는 독립적으로 처리를 시작하는 작업을 생성하고,
프로그램은 해당 작업이 실행되는 동안에도 다른 로직(UI 렌더링, 입력 처리 등)을 계속 실행합니다.

StartDownloadXXX가 작업을 생성합니다.
각 작업은 구현 방식에 따라 다르게 동작할 수 있습니다.· 동일한 스레드에서 실행될 수도 있고, 여러 프레임에 걸쳐 분산 실행될 수도 있습니다(프레임 분할).
그림 2. 단일 스레드 비동기 실행: 작업은 메인 스레드를 차단하지 않으면서 여러 프레임에 걸쳐 실행됩니다.B
· 또는 다른 스레드에서 동시에 실행될 수도 있습니다(백그라운드 실행).
Unity에서의 비동기 프로그래밍 소개(9)
하지만 둘 중 어떤 방식으로 동작하든 핵심 아이디어는 동일합니다.· 메인 스레드를 차단하지 않으면서 작업을 실행합니다. 처리가 진행되는 동안에도 프로그램이 계속 실행되도록 합니다.
한편 메서드 실행을 일시 중지하고 다른 작업이 완료될 때까지 기다릴 수도 있습니다.
이렇게 하면 메서드는 처리를 일시적으로 중단할 수 있으며, 스레드 자체는 다른 작업을 처리할 수 있는 상태로 유지됩니다.
Unity에서의 비동기 프로그래밍 소개(10)
핵심은 비동기 코드에서 대기가 차단을 의미하지 않는다는 것입니다.
메서드가 일시 중지된 동안 스레드는 다른 작업을 수행할 수 있으므로 프로그램은 중단 없이 원활하게 실행됩니다.
Unity에서 비동기 코드를 구현하는 두 가지 주요 방법은 코루틴과 Awaitable입니다.
코루틴
Unity 6에서 Awaitable이 도입되기 이전에는 주로 코루틴을 사용해서 비동기 동작을 구현했습니다.
코루틴은 실행을 일시 중단하고 재개하는 방식으로 비동기 동작을 구현하는 프로그래밍 패턴입니다.집을 스마트하게 청소하는 방식에 비유할 수 있겠습니다.
반면 동기 프로그래밍은 하루 내내 온 집안을 힘들게 청소하는 방식입니다.
private void CleaningHouse()
{
Debug.Log("Start Cleaning House");
CleanLivingRoom();
CleanBedRoom();
CleanBathRoom();
Debug.Log("Finish Cleaning house");
}
하지만 이렇게 하루 종일 온 집안을 힘들게 청소하는 대신, 청소를 나눠서 해 볼 수도 있습니다.· 1일차: 거실과 주방 청소하기· 2일차: 침실 청소하기· 3일차: 욕실 청소하고 마무리하기
이렇게 하면 특정한 날에만 무리하여 지칠 일이 없으며, 운동이나 가족과 보내는 시간,
또는 비디오 게임 같은 다른 중요한 일상도 충분히 병행할 수 있습니다.
CleaningHouse()를 코루틴으로 다시 작성하면 다음과 같습니다.
private IEnumerator CleaningHouseRoutine()
{
Debug.Log("Start Cleaning House");
CleanLivingRoom();
yield return new WaitForSeconds(10); // 10초 대기
CleanBedRoom();
yield return new WaitForSeconds(10); // 10초 대기
CleanBathRoom();
Debug.Log("Finish Cleaning house");
}
CleaningHouseRoutine() 코루틴이 실행되면 정확히 다음과 같은 일이 진행됩니다.
1단계:· Debug.Log(“Start Cleaning House”) 즉시 출력· CleanLivingRoom() 실행 및 완료
2단계:· 코루틴이 yield return new WaitForSeconds(10)에 도달· 코루틴은 여기서 일시 정지하고 종료되며, 제어 권한이 Unity로 돌아감· Unity는 다른 작업(렌더링, 입력 처리, 다른 시스템 실행 등)을 계속 수행· 코루틴은 백그라운드에서 10초 동안 대기
3단계(10초 후):· Unity는 코루틴을 중단된 지점에서 다시 시작· CleanBedRoom() 실행 및 완료
4단계:· 코루틴이 두 번째 yield return new WaitForSeconds(10)에 도달· 코루틴이 다시 일시 중지 및 종료되며 제어 권한이 Unity로 돌아감· 또 다른 10초 대기 시작
5단계(또다시 10초 후):· Unity가 코루틴을 다시 시작· CleanBathRoom() 실행 및 완료· Debug.Log(“Finish Cleaning house”) 출력· 코루틴이 끝에 도달하여 완료됨
코루틴을 이해하는 핵심은 yield 문입니다.
코루틴이 yield 문에 도달하면 정확히 그 지점에서 실행이 일시 중지되며 제어 권한이 Unity로 돌아갑니다.
예를 들어 코드가 `yield return SomeTask()`에 도달하면
프로그램은 `SomeTask()`를 실행하며 코루틴은 `SomeTask()`가 완료될 때까지 일시 중지됩니다.
이와 유사하게 `yield return new WaitForSeconds(n);`에 도달하면
코루틴은 n초 동안 일시 중지됩니다. 코루틴이 중단된 동안에는 다른 코드가 실행될 수 있습니다.
그리고 yield 문을 통해 대기 중인 시간이나 작업이 완료되면, 코드 실행은 코루틴 내의 마지막 지점에서 재개됩니다.
이제 16초 동안 게임을 일시 중지했던 동기식 코드를 코루틴으로 다시 작성할 수 있습니다.
// 코루틴 접근 방식: 일시 중지 및 재개
private IEnumerator LoadMainMenuRoutine()
{
// 1일차: 플레이어 에셋 다운로드 시작 및
// 완료를 기다리는 동안 잠시 휴식
yield return DownloadPlayerAssetsRoutine();
// 2일차: 월드 에셋 다운로드 시작 및
// 완료를 기다리는 동안 잠시 휴식
yield return DownloadWorldAssetsRoutine();
// 3일차: 오디오 에셋 다운로드 시작 및
// 완료를 기다리는 동안 잠시 휴식
yield return DownloadAudioAssetsRoutine();
ShowMainUI(); // 게임이 계속 응답 가능한 상태로 유지됨!
}
코루틴에 대한 자세한 설명은 아래의 공식 Unity 기술 자료에서 확인하세요.
참고: 비동기 단일 스레드
비동기 프로그래밍은 멀티스레딩과 관련이 있지만 멀티스레딩을 반드시 필요로 하지는 않습니다.
핵심은 작업이 완료될 때까지 기다리지 않고 시작하는 것이며,
따라서 비동기 작업은 여전히 같은 스레드에서 서로 다른 시점에 실행될 수 있습니다.
Unity에서 코루틴은 메인 스레드, 즉 StartCoroutine() 메서드를 호출하는 스레드에서 실행됩니다.
하지만 코루틴은 yield 문을 사용하여 여러 프레임에 걸쳐 일시 중지 및 재개가 가능하므로 프로그램의 응답성이 유지됩니다.
그림 5. 코루틴 프레임 플로 - 각 yield 문은 실행을 일시 중지하고 이후 프레임에서 재개됩니다.
예를 들어 이 다이어그램은 SomeCoroutineMethod()와 같은 코루틴 메서드가 yield 문을 사용하여
여러 프레임에 걸쳐 작업을 분할하는 방식을 보여 줍니다.
각각의 소규모 작업은 메인 스레드에서 실행되지만, 프레임 간 실행을 멈췄다 다시 시작하는 방식을 통해
장시간의 작업 중에도 게임이 멈추지 않고 매끄러운 응답성을 유지할 수 있습니다.
전통적인 코루틴 접근 방식의 문제
코루틴은 아주 유용하게 활용되어 왔지만, 불필요하게 복잡한 코드를 발생시키는 경우가 종종 있습니다.
코루틴을 사용하여 어드레서블 에셋을 로드하는 예를 살펴보겠습니다.
// 어드레서블 에셋을 비동기적으로 로드하는 코루틴
public IEnumerator GetAssetRoutine(string assetName, Action onAssetLoaded)
{
// 에셋 로딩을 시작하고 완료될 때까지 기다릴 수 있도록 핸들을 유지합니다
var asyncOperationHandle = Addressables.LoadAssetAsync(assetName);
// 로딩이 완료될 때까지 대기
yield return asyncOperationHandle;
// 핸들에서 로드된 에셋 가져오기
var loadedAsset = asyncOperationHandle.Result;
// 콜백을 통해 결과 전달
onAssetLoaded(loadedAsset);
}
public void Start()
{
// 다음과 같이 작성할 것으로 예상할 수 있습니다.
// GameObject loadedAsset = StartCoroutine(GetAssetRoutine("boat"));
// Debug.Log(loadedAsset);
// 하지만 코루틴은 값을 직접 반환할 수 없습니다.
// 그 대신 콜백을 사용해서 로드된 에셋을 받아야 합니다.
StartCoroutine(GetAssetRoutine("boat",(loadedAsset) => { Debug.Log(loadedAsset); }));
}
참고: Addressables는 에셋을 주소(키) 이름으로 참조하여 쉽게 로드할 수 있게 하는 패키지입니다.
이 패키지는 에셋 위치를 내부적으로 처리하여 코드를 간소화하므로
사용자가 파일 경로나 번들 구조를 직접 관리할 필요가 없습니다.
위 코드는 '='를 통해 로드된 에셋을 직접 가져올 수 없기 때문에 직관적이지 않습니다.
근본적인 문제는 Unity의 코루틴이 실제 데이터를 반환하는 것이 아니라 실
행 플로를 제어하는 데 사용되는 반복자인 IEnumerator를 반환한다는 점입니다.
호출자에게 반환 값을 전달하는 메커니즘이 없습니다.
따라서 코루틴에서 결과를 얻으려면 콜백을 사용하거나 데이터를 공유 변수에 저장해야 합니다.
이로 인해 불필요하게 복잡하고 직관적이지 않은 콜백 패턴을 사용하게 될 수 있습니다.
public IEnumerator GetAssetRoutine(string assetName, Action onAssetLoaded)
{
var asyncOperationHandle = Addressables.LoadAssetAsync(assetName);
yield return asyncOperationHandle;
var loadedAsset = asyncOperationHandle.Result;
onAssetLoaded(loadedAsset);
}
public void Start()
{
StartCoroutine(GetAssetRoutine("boat", (loadedAsset) => {
DoSomething(loadedAsset, (result1) => {
DoSomething2(result1, (result2) => {
DoSomething3(result2, (result3) => {
// 콜백 지옥에 오신 것을 환영합니다!
});
});
});
}));
}
전통적인 접근 방식의 추가적인 제약
코루틴에는 가독성 문제 외에도 여러 가지 기술적인 한계가 있습니다.· 메모리 할당 오버헤드: 각 코루틴은 IEnumerator 오브젝트를 생성하여 GC 부하를 유발합니다.· 메인 스레드 전용: 코루틴은 Unity의 플레이어 루프를 통해 메인 스레드에서만 실행됩니다.
C# async/await: 더 나은 방법
C#의 네이티브 async/await 구문은 더 명쾌한 해결책을 제공합니다.
public async Task GetAssetAsync(string assetName)
{
// 에셋 로딩을 시작하고 완료될 때까지 기다릴 수 있도록 핸들을 유지합니다
var asyncOperationHandle = Addressables.LoadAssetAsync(assetName);
// .Task가 비동기 작업을 Task로 노출하므로 await할 수 있습니다
// 로딩 작업이 완료될 때까지 기다립니다(차단하지 않음)
await asyncOperationHandle.Task;
// 로드된 에셋을 반환합니다
return asyncOperationHandle.Result;
}
public async void Start()
{
// 에셋이 로드될 때까지 기다립니다
GameObject loadedAsset = await GetAssetAsync("boat");
// 이후 비동기 작업을 순차적으로 실행합니다
await DoSomethingAsync(loadedAsset);
await DoSomething2Async(loadedAsset);
await DoSomething3Async(loadedAsset);
}
훨씬 깔끔해졌죠!
async 키워드는 메서드를 비동기 메서드로 지정하며,
await 키워드는 메서드 실행을 일시 중지하고 체크포인트를 생성합니다.
await 중인 작업이 완료되면 해당 체크포인트에서 실행이 재개되어
메서드의 나머지 부분이 끊김 없이 이어져 실행됩니다.
async 메서드는 Task 또는 Task<T>를 반환하며, 여기서 T는 결과의 유형입니다.
Task<T> 를 await하면, await 표현식은 비동기 작업이 완료된 후에만 실행되며
T 유형의 결과를 자동으로 반환합니다.
따라서 다음과 같은 코드를 작성할 수 있습니다.
GameObject asset = await LoadAssetAsync("boat");
await 키워드는 Task를 언래핑하여 실제 결과를 제공하므로, 이를 직접 사용할 수 있습니다.
따라서 콜백이나 공유 변수에 의존하지 않고도 비동기 결과를 처리할 수 있어 코드가 훨씬 깔끔해집니다.
하지만 Unity 개발자들은 여러 가지 제한 때문에 과거에는 async/await를 잘 사용하지 않았습니다.다음 코드 예시를 살펴보겠습니다.
private async void Start()
{
var task = SomeAsync(); // 매번 Task 오브젝트가 생성됨
await task;
}
private async Task SomeAsync()
{
Time.timeScale = 2f;
Debug.Log($"Time: {Time.time}");
// 게임이 두 배 속도로 실행되고 있을 때도
// 여전히 실제 시간으로 100ms를 기다려야 합니다.
await Task.Delay(100);
Debug.Log($"Time: {Time.time}");
}
위 코드에는 다음과 같은 문제점이 있습니다.· 메모리 할당
비동기 메서드를 호출할 때마다 Task 오브젝트가 생성되어 GC 부하가 발생합니다.· Unity 라이프사이클 통합 불가
Task 메서드는 프레임 타이밍이나 FixedUpdate와 같은 Unity의 Playerloop 전용 이벤트를 await할 수 없습니다.
또한 시간 스케일에 따라 시간을 다르게 기다릴 수도 없습니다.
Awaitable 소개
Unity 6에서 도입된 Awaitable을 사용하여 비동기 메서드를 작성하면
async 및 await 키워드를 계속 사용하면서 위의 문제를 해결할 수 있습니다.
Unity 6 이후에서 비동기 프로그래밍을 구현할 때는 이 방식을 사용하는 것이 좋습니다.
구문은 다음과 같습니다.
public async Awaitable GetAssetAsync(string assetName)
{
var asyncOperationHandle = Addressables.LoadAssetAsync(assetName);
// .Task가 비동기 작업을 Task로 노출하므로 await할 수 있습니다
// 로딩 작업이 완료될 때까지 기다립니다(차단하지 않음)
await asyncOperationHandle.Task;
// 로딩이 완료되면 로드된 에셋을 반환합니다
return asyncOperationHandle.Result;
}
public async void Start()
{
var loadedAsset = await GetAssetAsync("boat");
}
Awaitable 풀링
Task와 다른 Awaitable의 장점 중 하나는 오브젝트 풀링 시스템입니다.
이 시스템 덕분에 비동기 작업마다 새로운 Task 오브젝트를 생성해야 하는 GC 부하가 줄어듭니다.
하지만 여기에는 치명적인 한계가 있습니다.
바로 동일한 Awaitable 인스턴스를 두 번 이상 await해서는 안 된다는 것입니다.
// 잘못된 방식
async Awaitable AwaitableExample()
{
var awaitableInstance = SomeAwaitableMethod();
await awaitableInstance; // 첫 번째 await - 정상적으로 완료
// 이 시점에서, awaitable 인스턴스는 풀로 돌아갑니다!
await awaitableInstance; // 두 번째 await - 정의되지 않은 동작!
}
원인
첫 번째 await가 완료되면 Awaitable 인스턴스는 Unity의 오브젝트 풀로 반환되어
다른 비동기 메서드에서 즉시 재사용될 수 있습니다.두 번째 await는 실제로는 완전히 다른 작업을 기다리는 것일 수도 있습니다.
다음 예시를 보면 이를 구체적으로 확인할 수 있습니다.
async void Start()
{
Awaitable awaitable1 = TestAsyncMethod();
await awaitable1;
Awaitable awaitable2 = TestAsyncMethod();
// 이는 풀링으로 인해 참일 가능성이 높습니다!
if(awaitable1 == awaitable2)
{
Debug.LogError("Same Awaitable instance reused from pool!");
}
// awaitable1은 이제 awaitable2의 작업을 나타낼 수도 있습니다
// 교착 상태 또는 예기치 않은 동작 발생 가능
await awaitable1;
}
async Awaitable TestAsyncMethod()
{
await Awaitable.WaitForSecondsAsync(1f);
}
Awaitable은 풀을 통해 재활용되므로 awaitable1과 awaitable2는 동일한 인스턴스입니다.
즉, awaitable1을 await하는 것은 awaitable2를 await하는 것과 실질적으로 동일합니다.
이는 의도치 않은 동작을 발생시킬 수 있습니다.
· 베스트 프랙티스: Awaitable 인스턴스는 항상 즉시 await하고 나중에 재사용할 목적으로 저장하지 않습니다.
async void Start(){
// 잘못된 방식: Awaitable 인스턴스를 저장해서 나중에 await
Awaitable awaitable = TestAsyncMethod();
await awaitable;
// 바람직한 방식: 즉시 await
await TestAsyncMethod();
}
Unity 플레이어 루프 통합
Task.Delay()와는 달리 Awaitable 메서드는 Unity의 시간 관리 방식을 따릅니다.
public class TimingComparison : MonoBehaviour
{
async void Start()
{
Debug.Log($"Starting at Time: {Time.time}");
Time.timeScale = 2f; // 게임 속도 2배
// 실제 시간 지연(timeScale의 영향을 받지 않음)
await Task.Delay(2000); // 항상 실제 2초에 해당하는 시간
Debug.Log($"Task.Delay completed at Time: {Time.time}");
// 게임 시간 지연(timeScale의 영향을 받음)
await Awaitable.WaitForSecondsAsync(2f); // 2배 속도에서 1초의 실제 시간
Debug.Log($"Awaitable completed at Time: {Time.time}");
Time.timeScale = 1f;
}
}
또한 Awaitable은 Playerloop 전용 Await 메서드를 제공합니다. // 시간 기반 대기
await Awaitable.WaitForSecondsAsync(1.5f); // Time.timeScale의 영향을 받음
// 프레임 기반 대기
await Awaitable.NextFrameAsync(); // 1프레임 대기
await Awaitable.EndOfFrameAsync(); // 현재 프레임의 끝
await Awaitable.FixedUpdateAsync(); // 다음 FixedUpdate
// 백그라운드 처리
await Awaitable.BackgroundThreadAsync(); // 백그라운드 스레드로 전환
await Awaitable.MainThreadAsync(); // 메인 스레드로 다시 전환스레드 전환
대부분의 Unity Object API(예: 게임 오브젝트 생성, 트랜스폼 수정, 씬 데이터 액세스 등)는 메인 스레드에서 호출해야 합니다.
하지만 메인 스레드에서 성능에 부담을 주는 연산을 직접 실행하면 프레임 속도 저하나 끊김 현상이 발생할 수 있으므로,
Awaitable을 사용해서 이러한 작업을 백그라운드 스레드로 오프로드하는 것이 좋습니다.
Awaitable은 스레드 간 전환을 위한 빌트인 메서드를 제공합니다.
await Awaitable.BackgroundThreadAsync(); // 백그라운드 스레드로 전환
await Awaitable.MainThreadAsync(); // 메인 스레드로 다시 전환
public class ThreadSwitchExample : MonoBehaviour
{
async void Start()
{
var results = await DoHeavyComputationAsync();
// 메인 스레드에서 자동으로 재개되므로 Unity API를 안전하게 사용할 수 있음
foreach (var position in results)
{
var go = new GameObject("ComputedObject");
go.transform.position = position;
}
}
async Awaitable DoHeavyComputationAsync()
{
// 메인 스레드를 차단하지 않기 위해 백그라운드 스레드로 전환
await Awaitable.BackgroundThreadAsync();
// CPU를 많이 소모하는 계산
var results = new Vector3[10000];
for (int i = 0; i < results.Length; i++)
{
results[i] = new Vector3(
Mathf.Sin(i * 0.1f),
Mathf.Cos(i * 0.1f),
Mathf.Tan(i * 0.1f)
);
}
// Awaitable.MainThreadAsync()를 호출할 필요 없음
// 이어서 실행될 코드가 메인 스레드에서 자동으로 재개
return results;
}
}Unity Object API를 호출해서는 안 됩니다.
해당 API는 스레드 안전성이 보장되지 않기 때문입니다.
Awaitable.MainThreadAsync와 Awaitable.BackgroundThreadAsync의 효과는 현재 메서드 내에만 적용됩니다.
Start()는 메인 스레드에서 실행되므로, await DoHeavyComputationAsync() 이후에 이어서 실행해야 하는 코드도
메인 스레드에서 재개됩니다.
· 참고 : 극도로 높은 성능이 필요하다면 비동기 스레딩 대신 Unity의 잡 시스템과 버스트 컴파일러를 사용하는 것을 고려해 보세요.
1프레임 지연 규칙
스레드를 전환하면 1프레임의 지연이 발생하는데,
이는 Unity가 이어서 실행될 코드를 대기열에 추가하여 다음 업데이트 사이클에 처리되도록 만들기 때문입니다.
· 동일 스레드로 전환하면 지연이 발생하지 않습니다.· 너무 자주 전환하면 불필요한 오버헤드가 발생할 수 있습니다.
async Awaitable PerformanceExample()
{
await Awaitable.MainThreadAsync(); // 지연 없음(이미 메인 스레드에 있음)
await Awaitable.BackgroundThreadAsync(); // 다음 줄에 대한 1프레임 지연
DoBackgroundWork();
await Awaitable.MainThreadAsync(); // 또 다른 1프레임 지연
DoMainThreadWork();
}
따라서 성능이 중요한 코드에서는 빈번한 스레드 전환을 지양해야 합니다. // 잘못된 방식: 과도한 스레드 전환
for (int i = 0; i < 100; i++)
{
await Awaitable.BackgroundThreadAsync(); // 오버헤드 과다!
DoSmallWork();
await Awaitable.MainThreadAsync();
}요약
Unity의 비동기 프로그래밍은 메인 스레드를 차단하지 않으면서 작업을 실행할 수 있게 하므로,
이를 활용하면 장시간의 작업 중에도 게임의 응답성을 유지할 수 있습니다.
Unity 개발자들은 코루틴을 사용해서 비동기 동작을 구현했습니다.
코루틴은 효과적이지만 코드가 장황해질 수 있고, 값을 직접 반환할 수 없으며,
복잡한 콜백 체인을 발생시키기도 합니다.
C#의 async/await 패턴은 직접 반환 값을 제공하는 더 깔끔하고 직관적인 비동기 코드를 제공하지만,
표준 Task 오브젝트는 가비지 컬렉션 오버헤드를 발생시키고
Unity의 플레이어 루프 및 시간 스케일과 통합되지 않습니다.
Unity 6의 Awaitable 유형은 다음과 같은 방식으로 이러한 격차를 해소합니다.· 오브젝트 풀링을 통한 GC 부하 제거· Unity의 라이프사이클 및 시간 스케일과의 통합· 프레임, 업데이트, 스레드에 대한 Unity 전용 await 메서드 제공· 메인 스레드와 백그라운드 스레드 간의 원활한 스레드 전환 허용
베스트 프랙티스:· Awaitable 인스턴스는 항상 즉시 await하고, 완료된 이후에는 절대 재사용하지 않습니다.
· 성능이 중요한 코드에서는 스레드 전환을 최소화합니다.· 간단한 순차적 지연에는 코루틴을 사용하고, 복잡한 비동기 워크플로에는 async/await와 Awaitable을 함께 사용합니다.· Unity 오브젝트와 상호 작용하기 전에 메인 스레드로 다시 전환합니다.
코루틴, async/await, Awaitable을 이해하면 각 시나리오에 맞는 툴을 선택하고,
더욱 깔끔한 비동기 코드를 작성하고, 불필요한 차단을 발생시키지 않으면서
Unity 게임을 원활하게 실행할 수 있습니다.
다음으로 살펴볼 내용
이 글에서는 Unity에서 비동기 프로그래밍을 구현하는 방법인
코루틴, async/await, Unity 6의 Awaitable에 대해 알아보았습니다.
하지만 한 가지 중요한 기본 개념인 SynchronizationContext는 아직 다루지 않았습니다.
기억해 둬야 할 포인트는 다음과 같습니다.
· 비동기 프로그래밍은 멀티스레딩과 밀접한 관련이 있지만, 둘은 같은 개념이 아닙니다.· 비동기 프로그래밍에서 async/await는 코드 실행을 일시 중지하고 재개하는 역할을 합니다.· SynchronizationContext는 비동기 작업이 재개된 후 어떤 스레드 또는 환경에서 계속 실행될지를 나타냅니다.· Unity는 대부분의 비동기 작업이 메인 스레드에서 실행되도록 UnitySynchronizationContext라는
특수 컨텍스트를 사용하여 Unity API와의 안전한 상호 작용을 보장합니다.
이 주제에 대해 더욱 자세히 알아보려면 SynchronizationContext와 Unity의 구현에 관하여
제가 작성한 후속 자료를 확인해 보세요.
