Version: Unity 6 (6000.0)
Language : English
Code optimization
Job system

Asynchronous programming

Unity supports a simplified asynchronous programming model using the .NET async key word and await operator. Refer to Asynchronous programming with async and await in the Microsoft documentation for an introduction to asynchronous programming in .NET.

Most of Unity’s asynchronous API supports the async and await pattern, including:

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

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

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

In most cases, you should prefer using Awaitable over .NET Task when creating an async method.

System.Threading.Tasks.Task vs UnityEngine.Awaitable

Unity’s Awaitable class is designed to be as efficient as possible for use in Unity 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.

Note: 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.Tasks.Task UnityEngine.Awaitable
Can be awaited multiple times Yes No
Can return a value Yes, using System.Threading.Tasks.Task<T> Yes, using UnityEngine.Awaitable<T>
Completion can be triggered by code Yes, using System.Threading.Tasks.TaskCompletionSource Yes, using UnityEngine.AwaitableCompletionSource
Continuations are run asynchronously Yes, by default using the synchronization context, otherwise using the ThreadPool No, continuation is run synchronously when completion is triggered

Task continuations and synchronization context

System.Threading.Tasks.Task continuations run in the synchronization context that was active when the API was called, or through the thread pool if none was set. An async method awaiting a Task will resume in the ambient synchronization context. In the Unity context, the continuation is posted to the UnitySynchronizationContext and executed on the next Update tick on the main thread. Consider the following example:

void PrintA()
{
    DoSomething();

    for (int i = 0; i < 100000; ++i)
    {
        // do something else
        print("A");
    }
}

async void DoSomething()
{
    await Task.Delay(1);
    print("Do something"):
}

If you run this code in Unity, “Do something” will not appear until after the last “A” because PrintA is not an async method and blocks the main thread until it completes.

Awaitable continuations and synchronization context

Awaitable continuations run synchronously when the operation completes. Most of the Unity API isn’t thread-safe and therefore, you should only use Unity APIs from the main thread. Unity overwrites the default SynchronizationContext with a custom UnitySynchronizationContext and runs all the .NET Tasks continuations on the main thread in both Edit and Play modes by default.

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

Unity doesn’t automatically stop code running in the background when you exit Play mode. To cancel a background operation on exiting Play mode, use Application.exitCancellationToken.

In development buildsA development build includes debug symbols and enables the Profiler. More info
See in Glossary
, Unity displays the following error message if you try to use Unity APIs in multithreaded code:


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.

Important: For performance reasons, Unity doesn’t perform checks for multithreaded behavior in non-development builds and doesn’t display this error in live builds. This means that while Unity doesn’t prevent execution of multithreaded code on live builds, random crashes and other unpredictable errors are likely if you do use multiple threads. Thus when writing code potentially running in a background thread, you need to be extra-careful which APIs you are calling. For this reason, you shouldn’t use your own multithreading and instead use Unity’s job system. The job system uses multiple threads safely to execute jobs in parallel and achieve the performance benefits of multithreading. For more information, see Job system overview.

Using Awaitable.BackgroundThreadAsync and getting back on main thread with Awaitable.MainThreadAsync is suitable for relatively long-running background operations (e.g. longer than a frame), to avoid blocking the main game loop, but is not suitable for taking advantage of multi-core CPUs within a single frame.

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

Performance considerations

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

Calling Task-returning methods from the main thread

If you call a Task-returning method from the Unity main thread, the continuation executes in the main frame. If you call it from a background thread, it completes on a thread pool thread. Calling a Task-returning API from main thread can increase latency. If it doesn’t complete synchronously, you’ll need to wait at least for the next Update tick (33ms at 30fps) for the continuation to run.

If network latency is a concern, it’s recommended to do this off the main thread and use custom logic to synchronize between the main thread and networkingThe Unity system that enables multiplayer gaming across a computer network. More info
See in Glossary
tasks.

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.

Testing

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

Examples

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:

public async Awaitable Start()
{
    await CallSomeThirdPartyAPIReturningDotnetTask();
    await Awaitable.NextFrameAsync();
    await SceneManager.LoadSceneAsync("my-scene");
    await SomeUserCodeReturningAwaitable();
    ...
}

Additional resources

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