Awaitable completion and continuation
Understand how asynchronous code resumes on completion of an awaited task and how this affects the function and performance of your application.
Read time 5 minutesLast updated 14 days ago
The operator suspends execution of the enclosing async method, which allows the calling thread to perform other work while waiting. When the awaited or completes, the asynchronous code needs to resume and continue its execution from the point it was suspended. How asynchronous code resumes can have important effects on the function and performance of your application.
awaitTaskAwaitable.NET Task continuations
Information about the state code was in when it began awaiting is referred to as the synchronization context. The .NET platform provides the class for capturing this type of information. continuations run in the synchronization context from which the asynchronous method was called, or through the thread pool if no synchronization context was set.
SynchronizationContextTaskMost Unity APIs aren't thread-safe and can only be called from the main thread. For this reason, Unity overwrites the default with a custom to ensure all .NET continuations in both Edit mode and Play mode run on the main thread by default. If you call a -returning method from the Unity main thread, the continuation is posted to the and runs on the next frame tick on the main thread. If you call it from a background thread, it completes on a thread pool thread.
SynchronizationContextUnitySynchronizationContextTaskTaskUnitySynchronizationContextUpdateCapturing a synchronization context increases the performance overhead of your application and waiting for the next frame Update to resume on the main thread introduces latency at scale. You can avoid both these issues by using instead.
AwaitableAwaitable continuations
Unless documented otherwise, all instances returned by Unity APIs, as well as any user defined returning methods, have the following continuation scheduling behaviour:
Awaitableasync Awaitable- If the method is called from the main thread, it resumes on the main thread.
- Otherwise it resumes on a .NET thread.
ThreadPool
The notable exceptions to that are:
- : continuation happens on the main thread.
Awaitable.MainThreadAsync - : continuation happens on a background thread.
Awaitable.BackgroundThreadAsync
The effect of and are local to the current method only, for example:
Awaitable.MainThreadAsyncAwaitable.BackgroundThreadAsyncprivate async Awaitable<float> DoHeavyComputationInBackgroundAsync(){ await Awaitable.BackgroundThreadAsync(); // here we are on a background thread // do some heavy math here return 42; // note: we don't need to explicitly get back to the main thread here, depending on the caller thread, DoHeavyComputationInBackgroundAsync will automatically complete on the correct one.}public async Awaitable Start(){ var computationResult = await DoHeavyComputationInBackgroundAsync(); // although DoHeavyComputationInBackgroundAsync() internally switches to a background thread to avoid blocking, // because we await it from the main thread, we also resume execution on the main thread and can safely call "main thread only APIs" such as LoadSceneAsync() await SceneManager.LoadSceneAsync("my-scene"); // this will succeed as we resumed on main thread}
Thread switching and performance
It's most efficient to call from the main thread and from a background thread because in each case the code resumes immediately on completion. If you switch back to the main thread from a background thread with , your code can't resume until the next frame update on the main thread.
await Awaitable.MainThreadAsync()await Awaitable.BackgroundThreadAsync()MainThreadAsyncIf you call a -returning API from the main thread and it doesn't complete synchronously, you'll need to wait at least for the next 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 networking tasks.
TaskUpdateIn development builds, 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.
Awaitable compared to the job system
Unity's class is better suited to the following scenarios than the job system:
Awaitable- Simplifying code when dealing with inherently asynchronous operations, such as manipulating files or performing web requests, in a non-blocking way.
- Offloading long-running tasks (>1 frame) to a background thread.
- Modernizing iterator-based coroutines.
- Awaiting multiple kinds of asynchronous operations (frame events, Unity events, third party asynchronous APIs, I/O).
However, it's not recommended for shorter-lived operations such as parallelizing computationally-intensive algorithms. To get the most of multi-core CPUs and parallelize your algorithms, use the job system instead.
Triggering completion from code
AwaitableCompletionSourceAwaitableCompletionSource<T>Awaitablepublic class UserNamePrompt : MonoBehaviour { 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 : MonoBehaviour { ... 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); }}