UnityAsync is a coroutine framework for Unity built around the async API. This is not only a more efficient and effective replacement of Unity's IEnumerator coroutines, but also seamlessly integrates Unity with .NET 4+ asynchronous APIs.
With this library you can:
- Write allocation-free coroutines
- Seamlessly integrate with Task-based and async APIs
- Integrate with IEnumerator-based coroutines and YieldInstructions
- Easily switch sync contexts (main to background and vice-versa)
- Define your own custom await instructions (allocation free, no boxing!)
- Return results at the end of your coroutine
Rest assured; UnityAsync coroutines will generally perform better than Unity's built-in coroutines because:
- They rarely cause heap allocations
- They don't weave in and out of native code
- They don't rely on a monolithic state machine
Comparing with standard yield return null
update loops, performance is 150% improved. Uncached WaitForSeconds
loops yield a performance increase of over 290%. The benchmarks included the time it took to instantiate the coroutines.
The one caveat is, in order to store the coroutine, a Task
object must be allocated. A Task
object is a bigger allocation compared to a Coroutine
object. No way around it, if you want to integrate with Task
-based APIs, you need to use Tasks
. Often, you just need to fire the coroutine and forget about it, and in such cases, simply use void
return type to avoid allocations altogether.
Clone the repo into your assets folder or download the latest release and open the .unitypackage.
Let's say we want to replace a pretty straight-forward update loop IEnumerator
coroutine:
using UnityEngine;
using System.Collections;
...
IEnumerator UpdateLoop()
{
while(true)
yield return null;
}
void Start()
{
StartCoroutine(UpdateLoop());
}
...
UnityAsync coroutines are defined by async methods, which can return void
, Task
, or Task<TResult>
:
using UnityEngine;
using UnityAsync;
...
async void UpdateLoop()
{
while(true)
await new WaitForFrames(1);
}
void Start()
{
UpdateLoop();
}
...
Easy-peasy, right? WaitForFrames is an IAwaitInstruction
. When you await it, it spawns a Continuation<T>
, which is (automatically) inserted into a queue and evaluated every frame until it is finished; in this case it will take one frame. Once finished, whatever is after the IAwaitInstruction
is invoked. We could return Task
instead of void
if we wanted to store the coroutine for nesting.
If you want to link the lifetime of an async coroutine (or part of the coroutine) to a UnityEngine.Object
, like the built-in Coroutine
s do, see the ConfigureAwait section.
Let's say we have some work we want to perform across the main thread, but it's too much to perform in a single frame. We can create a coroutine for this and return the result of the work once it is finished.
using UnityEngine;
using UnityAsync;
using System.Threading.Tasks;
...
async Task<int> GenerateResult()
{
// contrived examples are the best examples, right?
int result = 1;
for(int i = 0; i < 10; ++i)
{
for(int j = 0; j < 100000; ++j)
result = Mathf.Sin(result * j);
await new WaitForFrames(1);
}
return result;
}
async void Start()
{
int result = await GenerateResult();
// ...10 frames later
Debug.Log(result);
}
...
We are awaiting the result in Start()
so it needs to be an async method (this doesn't impact on how Unity calls it). We now return Task<int>
and the result is available after 10 frames.
Sometimes you'll want to perform some task on the thread pool and return to the main thread once this is completed without blocking. This could be done via Task.ConfigureAwait()
but now you can await directly on a SynchronizationContext
which makes it super easy to swap back and forth:
using UnityAsync;
...
async void Start()
{
// do some work on the thread pool
await Await.BackgroundSyncContext();
// ...
// resume on the main thread
await Await.UnitySyncContext();
// update Transforms, etc.
}
...
Await
actually contains many shortcut functions to streamline your code.
Just like with Task
s, UnityAsync IAwaitInstruction
s can be configured. You can set the scheduler (Update, LateUpdate, FixedUpdate) and also link its lifespan to a UnityEngine.Object
and/or CancellationToken
. Anything after the await
line will not be reached if the IAwaitInstruction
's linked UnityEngine.Object
was destroyed. If a cancellation was requested, an exception is thrown after the await
line, which can be handled as you would when a Task
is cancelled.
using UnityEngine;
using UnityAsync;
...
async void Start()
{
// continuation will finish if:
// - 10 seconds pass or
// - this MonoBehaviour is destroyed
// time is evaluated every fixed update
await Await.Seconds(10).ConfigureAwait(this, FrameScheduler.FixedUpdate);
// ... if the calling MonoBehaviour was destroyed, we won't get this far
// if it is still alive, we'll be in FixedUpdate
var c = new CancellationSource(100);
try
{
await Await.Seconds(10).ConfigureAwait(c.Token);
}
catch(OperationCancelledException)
{
// exception will be caught here after 100ms
}
}
...
You may run into a situation where some of your coroutine code is async, but it is called from an IEnumerator
. In such a situation you can use Task.AsYieldInstruction
or Task<TResult>.AsYieldInstruction
.
using UnityEngine;
using UnityAsync;
...
IEnumerator Start()
{
yield return new WaitForSeconds(1);
Debug.Log("Click to continue...");
yield return WaitForMouse().AsYieldInstruction();
Debug.Log("Click!");
}
async Task WaitForMouse()
{
await Await.Until(() => Input.GetMouseDown(0));
}
...
Built-in:
WaitForFrames
WaitForSeconds
WaitForSecondsRealtime
WaitUntil
1WaitWhile
1
Unity:
IEnumerator
2YieldInstruction
2AsyncOperation
ResourceRequest
3
Others:
Task
Task<>
- …anything that implements
GetAwaiter()
1Use the generic variants to pass a state object and avoid closures.
2Will spin up a Unity Coroutine
, causing allocations. Note, CustomYieldInstruction
implements IEnumerator
.
3Very small delegate allocation.
Anything that implements IAwaitInstruction
can be awaited and will be evaluated in Update, LateUpdate, or FixedUpdate. This is how the built-in await instructions are implemented. The advantage over Task
s or CustomYieldInstruction
s is they don't cause any allocations if implemented as structs.
You can implement your own await instructions by implementing IAwaitInstruction
.
public interface IAwaitInstruction
{
bool IsCompleted();
}
It's dead simple, just place your behaviour in IsCompleted
and return true
when your criteria are met. IsCompleted
will be evaluated every frame (depending on the FrameScheduler
). Use a struct to avoid unnecessary allocations.
One more thing to note; GetAwaiter()
is already provided through an extension method so that the instruction can encapsulate itself in an AwaitInstructionAwaiter
when awaited. It is this struct which actually ends up being awaited - as a kind of decoration to make custom await instructions more straight-forward to implement (the alternative is inheritance which causes heap allocations).
using UnityAsync;
public struct WaitForTimeSpan : IAwaitInstruction
{
readonly float finishTime;
bool IAwaitInstruction.IsCompleted() => AsyncManager.currentTime >= finishTime;
public WaitForFrames(TimeSpan timeSpan)
{
// we use AsyncManager.currentTime here (and above) because it's slightly more efficient vs Time.time
finishFrame = AsyncManager.currentTime + (float)timeSpan.TotalSeconds;
}
}
When running unit tests, AsyncManager
is not automatically initialized. Call AsyncManager.InitializeForEditorTests()
in your test OneTimeSetUp method, for example like this:
// In your test class
[OneTimeSetUp]
public void OneTimeSetUp()
{
AsyncManager.InitializeForEditorTests();
// Other setup code here
}