Unity supports a simplified asynchronous programming model using the .NET async key word and await operator. 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>> 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);
}
In most cases, you should prefer using Awaitable over .NET Task when creating an async method.
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 anAwaitable
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. 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
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:
public async Awaitable Start()
{
await CallSomeThirdPartyAPIReturningDotnetTask();
await Awaitable.NextFrameAsync();
await SceneManager.LoadSceneAsync("my-scene");
await SomeUserCodeReturningAwaitable();
...
}