Version: 2023.2
言語: 日本語
Null Reference Exception
重要なクラス

Await support

Unity 2023.1 introduces support for a simplified asynchronous programming model using C# async and await keywords. Most of Unity’s asynchronous API supports the async/await pattern, including:

You can also use the Awaitable class with both the await keyword and as an async return type in your own code, like this:

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 vs UnityEngine.Awaitable

Unity’s Awaitable class is designed to be as efficient as possible for use in Unity game or app projects, however this efficiency comes with some trade-offs compared with .NET tasks. The most significant limitation is that Awaitable instances are pooled to limit allocations. For example, consider the following example:

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

Without pooling, each instance of this behavior would allocate an Awaitable object each frame. This would put a lot of pressure on the garbage collector causing performance to suffer. To mitigate that, once awaited, Unity returns the Awaitable object to the internal Awaitable pool. This has one significant implication: you should never await more than once on an Awaitable instance. Doing so can result in undefined behavior such as an exception or a deadlock.

The following table lists the other notable differences and similarities between Unity’s Awaitable class and .NET Tasks.

  System.Threading.Task UnityEngine.Awaitable
Can be awaited multiple times あり なし
Can return a value Yes, using System.Threading.Task<T> Yes, using UnityEngine.Awaitable<T>
Completion can be triggered by code Yes, using System.Threading.TaskCompletionSource Yes, using UnityEngine.AwaitableCompletionSource
Continuation are run asynchronously Yes, by default using the synchronization context, otherwise using the ThreadPool No, continuation is run synchronously when completion is triggered

Awaitable, continuations and SynchronizationContext

As shown in the table above, Awaitable continuations are run synchronously when the operation completes. When not documented otherwise, all Awaitables returned by Unity APIs complete on the main thread, so there is no need to capture a synchronization context. However, you can write your code to do otherwise. See the following example:

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
}

To improve the situation, you should make sure your Awaitable-returning methods complete on the main thread by default. Here is an example where DoSomeHeavyComputationInBackgroundAsync completes on the main thread by default, while allowing callers to explicitly continue on the background thread (to chain heavy compute operations on a background thread without synchronizing back to the main thread):

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 and AwaitableCompletionSource<T> allows creations of Awaitable instances where completion is raised from user code. For example this can be used to elegantly implement user prompts without having to implement a state machine to wait for the user interaction to finish:


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 vs Iterator-based coroutines

In most cases, Awaitable should be slightly more efficient than iterator-based coroutines, especially for cases where the iterator would return non-null values (such as WaitForFixedUpdate etc).

Scalability

Although Unity’s Awaitable class is optimized for performance, you should avoid running hundreds of thousands of concurrent coroutines. Similar to coroutine-based iterators, a behavior with a loop similar to the following example attached to all your game objects is very likely to cause performance problems:

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

MainThreadAsync / BackgroundThreadAsync

From a performance perspective, it is very efficient to call await Awaitable.MainThreadAsync() from the main thread, or to call await Awaitable.BackgroundThreadAsync() from a background thread. However, the synchronization mechanism switching to the main thread from a background thread causes your code to resume on the next Update event. Therefore you should avoid from switching back and forth between the main thread and background threads at a high frequency, because your code will have to wait for the next frame on each call to MainThreadAsync().

Await support compared to the C# Job System

Unity’s Await support is better suited to the following scenarios than the C# Job system:

  • Simplifying code when dealing with inherently asynchronous operations such as manipulating files or performing web requests, in an non-blocking way.
  • Offloading long running tasks (>1 frame) to a background thread.
  • Modernizing Iterator-based coroutines.
  • Mixing and matching multiple kinds of async operations (frame events, unity events, third party asynchronous APIs, I/O).

However it is not recommended for shorter-lived operations such as when parallelizing computationally-intensive algorithms. To get the most of multi-core CPUs and parallelize your algorithms, you should instead use the C# Job System.

テスト

Unity’s Test Framework does not recognize Awaitable as a valid test return type. However, the following example shows how you can use Awaitable’s implementation of IEnumerator to write async tests:

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

Frame Coroutines

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()
{
    ...
}

Switching to a background thread and back

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

Loading resources asynchronously

public async Awaitable Start()
{
    var operation = Resources.LoadAsync("my-texture");
    await operation;
    var texture = operation.asset as Texture2D;
}

Composition

One of the biggest advantage of leveraging await, is that we can mix an match any await-compatible type in the same method: lang-csharp public async Awaitable Start() { await CallSomeThirdPartyAPIReturningDotnetTask(); await Awaitable.NextFrameAsync(); await SceneManager.LoadSceneAsync("my-scene"); await SomeUserCodeReturningAwaitable(); ... }

Null Reference Exception
重要なクラス
Copyright © 2023 Unity Technologies
优美缔软件(上海)有限公司 版权所有
"Unity"、Unity 徽标及其他 Unity 商标是 Unity Technologies 或其附属机构在美国及其他地区的商标或注册商标。其他名称或品牌是其各自所有者的商标。
公安部备案号:
31010902002961