Version: 2023.2
언어: 한국어
Null 레퍼런스 예외
중요 클래스

Await 지원

Unity 2023.1에서는 C# async 및 await 키워드를 사용하여 간소화된 비동기 프로그래밍 모델을 지원합니다. Unity의 비동기 API는 대부분 다음을 포함하는 async/await 패턴을 지원합니다.

다음과 같이 코드에서 Awaitable 클래스를 await 키워드와 함께 사용하거나 async 반환 유형으로 사용할 수도 있습니다.

async Awaitable<List<Achievement>> GetAchivementsAsync()
{
    var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
    JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
}

async Awaitable ShowAchievementsView()
{
    ShowLoadingOverlay();
    var achievements = await GetAchievementsAsync();
    HideLoadingOverlay();
    ShowAchivementsList(achievements);
}

System.Threading.Task와 UnityEngine.Awaitable 비교

Unity의 Awaitable 클래스는 Unity 게임 또는 앱 프로젝트에서 최대한 효율적으로 사용할 수 있도록 설계되었지만, .NET 작업과 비교했을 때 몇 가지 단점이 있습니다. 가장 큰 제한 사항은 Awaitable 인스턴스가 풀링되어 할당을 제한한다는 점입니다. 예를 들어 다음 예시를 고려해 보십시오.

class SomeMonoBehaviorWithAwaitable : MonoBehavior
{
    public async void Start()
    {
        while(true)
        {
            // do some work on each frame
            await Awaitable.NextFrameAsync();
        }
    }
}

풀링이 없으면 이 동작의 각 인스턴스는 프레임마다 Awaitable 오브젝트를 할당합니다. 이 경우 가비지 컬렉터에 많은 부담을 주어 성능이 저하될 수 있습니다. 이를 완화하기 위해 Unity는 대기 상태가 되면 Awaitable 오브젝트를 내부 Awaitable 풀로 반환합니다. 이 경우 중요한 한 가지 영향이 있는데, Awaitable 인스턴스에서 두 번 이상 대기해서는 안 됩니다. 그렇게 하면 예외나 교착 상태와 같이 정의되지 않은 동작이 발생할 수 있습니다.

다음 표에는 Unity의 Awaitable 클래스와 .NET 작업의 기타 주목할 만한 차이점과 유사점이 나와 있습니다.

  System.Threading.Task UnityEngine.Awaitable
여러 번 대기 가능 지원 지원 안 함
값 반환 가능 예(System.Threading.Task<T> 사용) 예(UnityEngine.Awaitable<T> 사용)
코드로 완료를 트리거할 수 있음 예(System.Threading.TaskCompletionSource 사용) 예(UnityEngine.AwaitableCompletionSource 사용)
연속성이 비동기적으로 실행됨 예(기본적으로 동기화 컨텍스트를 사용하고, 그렇지 않은 경우 ThreadPool을 사용) 아니요(완료가 트리거될 때 연속성이 동기적으로 실행됨)

Awaitable, 연속성 및 SynchronizationContext

위의 표에서 볼 수 있듯이, 작업이 완료되면 Awaitable 연속성이 동기적으로 실행됩니다. 달리 기술 자료에 나와 있지 않은 경우, Unity API가 반환하는 모든 Awaitable은 메인 스레드에서 완료되므로 동기화 컨텍스트를 캡처할 필요가 없습니다. 그러나 그렇게 하지 않도록 코드를 작성할 수 있습니다. 다음 예시를 참조하십시오.

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync()
{
    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 DoSomeHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this will fail as SceneManager.LoadAsync only works from main thread
}

상황을 개선하려면 기본적으로 메인 스레드에서 Awaitable 반환 메서드가 완료되도록 해야 합니다. 다음은 DoSomeHeavyComputationInBackgroundAsync가 기본적으로 메인 스레드에서 완료되지만 호출자가 명시적으로 백그라운드 스레드에서 계속할 수 있도록 하는 예제입니다. 이는 메인 스레드로 다시 동기화하지 않고 백그라운드 스레드에서 복잡한 컴퓨팅 연산을 연계하기 위함입니다.

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync(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 DoSomeHeavyComputationInBackgroundAsync();
    await SceneManager.LoadSceneAsync("my-scene"); // this is ok!
}

AwaitableCompletionSource

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 static 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);
    }
}

성능 고려 사항

Awaitable과 반복자 기반 코루틴 비교

대부분의 경우, 특히 반복자가 null이 아닌 값을 반환하는 경우(예: WaitForFixedUpdate 등)에는 Awaitable이 반복자 기반 코루틴보다 좀 더 효율적일 것입니다.

확장성

Unity의 Awaitable 클래스는 성능에 최적화되어 있지만, 수십만 개의 코루틴을 동시에 실행하는 것은 피해야 합니다. 코루틴 기반 반복자와 마찬가지로, 모든 게임 오브젝트에 다음 예제와 유사한 루프가 연결된 동작은 성능 문제를 일으킬 가능성이 매우 높습니다.

while(true){
    // do something
    await Awaitable.NextFrameAsync();
}

MainThreadAsync/BackgroundThreadAsync

성능의 관점에서 보면 메인 스레드에서 await Awaitable.MainThreadAsync()를 호출하거나 백그라운드 스레드에서 await Awaitable.BackgroundThreadAsync()를 호출하는 것이 매우 효율적입니다. 하지만 백그라운드 스레드에서 메인 스레드로 전환하는 동기화 메커니즘으로 인해 다음 업데이트 이벤트에서 코드가 다시 시작됩니다. 따라서 메인 스레드와 백그라운드 스레드 사이를 자주 전환하는 것은 피해야 합니다. 코드가 MainThreadAsync()를 호출할 때마다 다음 프레임을 기다려야 하기 때문입니다.

Await 지원과 C# 잡 시스템 비교

Unity의 Await 지원은 C# 잡 시스템보다 다음과 같은 시나리오에 더 적합합니다.

  • 파일 조작이나 웹 요청 수행과 같이 본질적으로 비동기적인 작업을 비차단 방식으로 처리하면 코드를 간소화할 수 있습니다.
  • 오래 실행되는 작업(1프레임 초과)을 백그라운드 스레드로 오프로드합니다.
  • 반복자 기반 코루틴을 현대화합니다.
  • 여러 종류의 비동기 작업(프레임 이벤트, Unity 이벤트, 타사 비동기 API, I/O)을 믹스 앤 매치합니다.

그러나 계산 집약적인 알고리즘을 병렬화하는 경우와 같이 짧게 실행되는 작업에는 권장되지 않습니다. 멀티 코어 CPU를 최대한 활용하고 알고리즘을 병렬화하려면 대신 C# 잡 시스템을 사용해야 합니다.

테스트

Unity의 테스트 프레임워크는 Awaitable을 유효한 테스트 반환 유형으로 인식하지 않습니다. 하지만 다음 예제는 Awaitable의 IEnumerator 구현을 사용하여 비동기 테스트를 작성하는 방법을 보여 줍니다.

[UnityTest]
public IEnumerator SomeAsyncTest(){
    async Awaitable TestImplementation(){
        // test something with async / await support here
    };
    return TestImplementation();
}

예제

프레임 코루틴

async Awaitable SampleSchedulingJobsForNextFrame()
{
    // wait until end of frame to avoid competing over resources with other unity subsystems
    await Awaitable.EndOfFrameAsync(); 
    var jobHandle = ScheduleSomethingWithJobSystem();
    // let the job execute while next frame starts
    await Awaitable.NextFrameAsync();
    jobHandle.Complete();
    // use results of computation
}

JobHandle ScheduleSomethingWithJobSystem()
{
    ...
}

백그라운드 스레드와 전환

private async Awaitable<float> DoSomeHeavyComputationInBackgroundAsync(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 operation = Resources.LoadAsync("my-texture");
    await operation;
    var texture = operation.asset as Texture2D;
}

합성

await을 활용할 때의 가장 큰 장점 중 하나는 동일한 메서드로 모든 await 호환 유형을 믹스 앤 매치하여 사용할 수 있다는 것입니다.

public async Awaitable Start()
{
    await CallSomeThirdPartyAPIReturningDotnetTask();
    await Awaitable.NextFrameAsync();
    await SceneManager.LoadSceneAsync("my-scene");
    await SomeUserCodeReturningAwaitable();
    ...
}
Null 레퍼런스 예외
중요 클래스