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);
}
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 | Yes | No |
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 |
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 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);
}
}
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).
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();
}
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().
Unity’s Await support is better suited to the following scenarios than the C# Job system:
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();
}
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;
}
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();
...
}