await 연산자는 둘러싸는 비동기 메서드의 실행을 일시 중단하여 호출 스레드가 대기하는 동안 다른 작업을 수행할 수 있게 합니다. 대기 중인 Task 또는 Awaitable이 완료되면 비동기 코드를 중단된 지점에서 다시 실행해야 합니다. 비동기 코드를 재시작하는 방식은 애플리케이션의 기능과 성능에 중대한 영향을 미칠 수 있습니다.
대기 시작 시 상태 코드에 대한 정보를 동기화 컨텍스트라고 합니다. .NET 플랫폼은 이러한 유형의 정보를 캡처할 수 있도록 SynchronizationContext 클래스를 제공합니다. Task 지속은 비동기 메서드가 호출된 동기화 컨텍스트에서 실행되며, 동기화 컨텍스트가 설정되지 않았을 때는 스레드 풀을 통해 실행됩니다.
대부분의 Unity API는 스레드 세이프가 아니며 메인 스레드에서만 호출할 수 있습니다. 이러한 이유로 Unity는 기본 SynchronizationContext를 커스텀 UnitySynchronizationContext로 덮어 써서, 편집 모드와 재생 모드의 모든 .NET Task 지속이 기본적으로 메인 스레드에서 실행되게 합니다. Unity 메인 스레드에서 Task 반환 메서드를 호출하면, 지속이 UnitySynchronizationContext에 게시되며 메인 스레드의 다음 프레임 Update 틱에서 실행됩니다. 이를 백그라운드 스레드에서 호출하면 스레드 풀 스레드에서 완료됩니다.
동기화 컨텍스트를 캡처하면 애플리케이션의 성능 오버헤드가 증가하며, 메인 스레드에서 다음 프레임 업데이트가 재개될 때까지 기다리면서 대규모 지연이 발생합니다. 대신 Awaitable을 사용하여 두 가지 문제를 모두 피할 수 있습니다.
달리 명시되지 않는 한, Unity API에서 반환된 모든 Awaitable 인스턴스는 메인 스레드에서 완료되므로 동기화 컨텍스트를 캡처할 필요가 없습니다. Awaitable 지속은 작업이 완료되면 동기적으로 실행되므로 코드가 즉시 재개될 수 있으며 다음 프레임을 기다릴 필요가 없습니다.
기본 지속 동작을 Awaitable.BackgroundThreadAsync를 통해 명시적으로 오버라이드할 수 있습니다. 다음 예시는 Awaitable이 배경 스레드에서 완료되도록 지정합니다.
private async Awaitable<float> DoHeavyComputationInBackgroundAsync()
{
await Awaitable.BackgroundThreadAsync();
// do some heavy math here
return 42; // this will make the returned awaitable complete on a background thread
}
public async Awaitable Start()
{
var computationResult = await DoHeavyComputationInBackgroundAsync();
await SceneManager.LoadSceneAsync("my-scene"); // this will fail as SceneManager.LoadAsync only works from main thread
}
이 경우 백그라운드 스레드에서 Awaitable을 완료하도록 설정하면 문제가 발생합니다. 백그라운드 스레드에서 Start 메서드가 재개되면 SceneManager.LoadSceneAsync는 메인 스레드에서만 작동하므로 다음 SceneManager.LoadSceneAsync 호출이 실패합니다.
이러한 상황을 바로 잡으려면 Awaitable.MainThreadAsync를 사용하여 Awaitable 반환 메서드가 기본적으로 메인 스레드에서 완료되게 하면서 백그라운드 스레드 작업 옵션을 여전히 제공할 수 있습니다. 다음 예시에서 DoHeavyComputationInBackgroundAsync는 기본적으로 메인 스레드에서 작업을 완료하기 전에 백그라운드 스레드에서 일부 작업을 수행합니다. continueOnMainThread 파라미터는 대규모 컴퓨팅 작업을 체이닝할 때와 같이, 적절한 경우에 백그라운드 스레드에서 완료할 수 있는 옵션을 호출자에게 제공합니다.
private async Awaitable<float> DoHeavyComputationInBackgroundAsync(bool continueOnMainThread = true)
{
await Awaitable.BackgroundThreadAsync();
// do some heavy math here
float result = 42;
// by default, switch back to main thread:
if(continueOnMainThread){
await Awaitable.MainThreadAsync();
}
return result;
}
public async Awaitable Start()
{
var computationResult = await DoHeavyComputationInBackgroundAsync();
await SceneManager.LoadSceneAsync("my-scene"); // this is ok!
}
참고: Unity는 플레이 모드가 종료되더라도 백그라운드에서 실행되는 코드를 자동으로 중지하지 않습니다. 플레이 모드를 종료할 때 백그라운드 작업이 취소되도록 하려면 Application.exitCancellationToken을 사용하십시오.
메인 스레드에서 await Awaitable.MainThreadAsync()를 호출하고 백그라운드 스레드에서 await Awaitable.BackgroundThreadAsync()를 호출하는 것이 가장 효율적입니다. 이는 각 경우에 코드가 완료되면 즉시 재개되기 때문입니다. MainThreadAsync가 있는 백그라운드 스레드에서 메인 스레드로 다시 전환할 경우, 메인 스레드의 다음 프레임 업데이트까지 코드를 재개할 수 없습니다.
메인 스레드에서 호출한 Task 반환 API가 동기식으로 완료되지 않을 경우, 적어도 다음 Update 틱(30fps에서 33ms)까지 기다려야 지속 실행이 시작됩니다. 네트워크 지연이 문제일 경우 메인 스레드에서 이 작업을 수행하고 커스텀 로직을 사용하여 메인 스레드와 네트워킹 작업을 동기화하는 것이 좋습니다.
개발 빌드에서 멀티 스레드 코드로 Unity API를 사용하려고 하는 경우 Unity는 다음과 같은 오류 메시지를 표시합니다.
UnityException: Internal_CreateGameObject can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
중요: 성능상의 이유로 Unity는 비개발 빌드에서 멀티스레드 동작을 확인하지 않으며 라이브 빌드에서는 이 오류를 표시하지 않습니다. Unity는 이러한 상황에서 멀티스레드 코드 실행을 막지는 않지만, 여러 스레드를 사용할 경우 크래시 및 기타 예측할 수 없는 오류가 발생할 수 있습니다. 자체 멀티스레딩을 사용하는 대신 Unity의 잡 시스템을 사용하는 것이 더 안전합니다. 잡 시스템은 여러 스레드를 안전하게 사용해 여러 잡을 병렬적으로 실행하여 멀티스레딩의 성능상 이점을 달성합니다. 자세한 내용은 잡 시스템 개요를 참조하십시오.
아래 시나리오에는 잡 시스템보다 Unity의 Awaitable 클래스가 더 적합합니다.
하지만 컴퓨팅 집약적인 알고리즘을 병렬화하는 등, 짧은 길이의 작업에는 적합하지 않습니다. 멀티 코어 CPU를 최대한 활용하고 알고리즘을 병렬화하려면 잡 시스템을 대신 사용하십시오.
AwaitableCompletionSource 및 AwaitableCompletionSource<T>로 사용자 코드에서 완료가 발생하는 Awaitable 인스턴스를 생성할 수 있습니다. 예를 들어 사용자 상호작용이 끝날 때까지 상태 머신 구현하지 않고서도 사용자 프롬프트를 구현하는 데 사용할 수 있습니다.
public class UserNamePrompt : MonoBehavior
{
TextField _userNameTextField;
AwaitableCompletionSource<string> _completionSource = new AwaitableCompletionSource<string>();
public void Start()
{
var rootVisual = GetComponent<UIDocument>().rootVisualElement;
var userNameField = rootVisual.Q<TextField>("userNameField");
rootVisual.Q<Button>("OkButton").clicked += ()=>{
_completionSource.SetResult(userNameField.text);
}
}
public Awaitable<string> WaitForUsernameAsync() => _completionSource.Awaitable;
}
...
public class HighScoreRanks : MonoBehavior
{
...
public async Awaitable ReportCurrentUserScoreAsync(int score)
{
_userNameOverlayGameObject.SetActive(true);
var prompt = _userNameOverlayGameObject.GetComponent<UserNamePrompt>();
var userName = await prompt.WaitForUsernameAsync();
_userNameOverlayGameObject.SetActive(false);
await SomeAPICall(userName, score);
}
}