Documentation

Unity Engine


User Manual

Script Reference

Unity Engine


Introduction to asynchronous programming with Awaitable

Understand the key features of Unity's Awaitable and how it compares to both .NET Task and iterator-based coroutines.
Read time 5 minutesLast updated 14 days ago

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:
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 compared to .NET Task

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 : MonoBehaviour{ public async void Start() { while(true) { // do some work on each frame await Awaitable.NextFrameAsync(); } }}
Without pooling, each instance of the
MonoBehaviour
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.

Awaitable compared to .NET ValueTask

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.

Awaitable, Task, and ValueTask summary

The following table summarizes the feature comparison between Unity's
Awaitable
class and .NET
Task
and
ValueTask
:

Feature

Task

ValueTask

UnityEngine.Awaitable

Required allocationsMany.
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 timesYes.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 asynchronouslyYes.
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 codeYes.
Using
TaskCompletionSource
.
Not applicable in the typical use case, which is for tasks that mostly complete synchronously.Yes.
Using
AwaitableCompletionSource
.
Can return a valueYes.
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 schedulingNo.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.

When to use Awaitable over Task or ValueTask

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:
    • You don't need to await your methods multiple times and expect them to mostly complete asynchronously.
    • You want your asynchronous tasks to have built-in support for Unity-specific concepts like the main thread and the Update and FixedUpdate loops.

Awaitable compared to iterator-based coroutines

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 GameObject in a large project.
Note
You can safely
yield return
an
Awaitable
from a traditional iterator-based coroutine, but you can't
yield return
an
Awaitable<T0>
.

Additional resources