The Awaitable class is a custom Unity type that can be awaited and used as an async return type in the C# asynchronous programming model. Most of Unity’s asynchronous APIs support the async and await pattern, including:
NextFrameAsync, WaitForSecondsAsync, EndOfFrameAsync, FixedUpdateAsync
AsyncOperation
You can use the Awaitable class with both the await operator and as an async return type in your own code, as follows:
async Awaitable<List<Achievement>> GetAchievementsAsync()
{
var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
List<Achievement> achievements = JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
return achievements;
}
async Awaitable ShowAchievementsView()
{
ShowLoadingOverlay();
List<Achievement> achievements = await GetAchievementsAsync();
HideLoadingOverlay();
ShowAchivementsList(achievements);
}
Awaitable is designed to offer a more efficient alternative to .NET Task for asynchronous code in Unity projects. The efficiency of Awaitable comes with some important limitations compared to Task.
The most significant limitation is that Awaitable instances are pooled to limit allocations. 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 the MonoBehavior in this example would allocate an Awaitable object each frame, increasing garbage collector workload and degrading performance. To mitigate this, Unity returns the Awaitable object to the internal Awaitable pool once it’s been awaited.
Important: The pooling of Awaitable instances means it’s never safe to await more than once on an Awaitable instance. Doing so can result in undefined behavior such as an exception or a deadlock.
The .NET ValueTask<TResult> offers some of the same key benefits and limitations of Awaitable. The typical recommended use for ValueTask is for asynchronous workloads that are expected to complete synchronously most of the time. For more information, refer to Understanding the Whys, Whats, and Whens of ValueTask.
The following table summarizes the feature comparison between Unity’s Awaitable class and .NET Task and ValueTask:
| Feature | Task |
ValueTask |
UnityEngine.Awaitable |
|---|---|---|---|
| Required allocations |
Many. Allocates on every call to a Task-returning method, increasing memory use and garbage collector workload. |
As-needed. Can be optimized with pooling. |
Minimal as-needed. Calling an Awaitable-returning method usually doesn’t allocate memory, since Awaitable instances are pooled by default. |
| Safe to await multiple times | Yes. |
No. Must convert to a Task with ValueTask.AsTask. |
No. Must convert to a Task with custom AsTask extension methods, refer to Awaiting multiple times in the same method in the code examples reference. |
| Continuations run asynchronously |
Yes. Using the synchronization context by default, otherwise using the ThreadPool. This increases latency when completing on the main thread in Unity because code must wait until the next frame Update to resume. |
Yes. Optimized for the case where awaited tasks complete synchronously. If they complete asynchronously, the continuation behavior is equivalent to Task. |
No. Continuation runs synchronously when completion is triggered, meaning code resumes immediately in the same frame in which completion is triggered. Refer to Awaitable completion and continuation for more information. |
| Completion can be triggered by code |
Yes. Using TaskCompletionSource. |
Not applicable in the typical use case, which is for tasks that mostly complete synchronously. |
Yes. Using AwaitableCompletionSource. |
| Can return a value |
Yes. Using Task<TResult>. |
Yes. Using ValueTask<TResult>. |
Yes. Using UnityEngine.Awaitable<T>. |
Built-in support for WaitAll and WaitAny |
Yes. |
No. Must convert to Task with ValueTask.AsTask. |
No. Must convert to a Task with custom AsTask extension methods, refer to Wrapping Awaitable in .NET Task in the code examples reference. |
| Unity thread and update loop-aware execution scheduling | No. | No. |
Yes. You can specify which thread an Awaitable resumes on with Awaitable.BackgroundThreadAsync and Awaitable.MainThreadAsync. You can also schedule work relative to the Update or FixedUpdate loops with Awaitable.NextFrameAsync and Awaitable.FixedUpdateAsync. For more information, refer to Awaitable completion and continuation. |
The choice of API depends on the performance profile of your asynchronous code, but in general:
Task is the only choice when you need to await multiple times or from several consumers concurrently.ValueTask is a good choice if you have high-throughput asynchronous code that completes synchronously most of the time.Awaitable is a good choice when:
Awaitable coroutines are usually more efficient than iterator-based coroutines, especially for cases where the iterator returns non-null values, such as WaitForFixedUpdate.
However, the performance advantage of Awaitable coroutines reduces when you run many of them concurrently. For example, a MonoBehaviour such as the one in the previous code example, which awaits Awaitable.NextFrameAsync in a while loop, is likely to cause performance problems if attached to every GameObjectThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
See in Glossary in a large project.