Async in Unity

Async in Unity (better or worse than coroutines?)

In Unity by John FrenchUpdated 20 Comments

When you write code in Unity, it typically happens all at once.

Meaning that every function that’s needed during a particular frame is processed before the frame is rendered.

Line by line, function by function, as quickly as possible.

However, a lot of what you do in a game happens over multiple frames, meaning that you may need to stage your game’s logic so that it takes place over a period of time instead.

You might normally do this with a Coroutine, which allows you to split up a function so that it takes place over a number of frames instead of just one.

However…

While coroutines can be extremely useful, they also have drawbacks.

What’s more, although they may appear to run separately to the rest of your code they are, in fact, synchronous operations.

Meaning that, if you do something in a coroutine that takes a long time to process, your game will pause while it completes.

So what’s the alternative?

Asynchronous Functions in Unity allow you to process logic without blocking the main thread, meaning that heavy tasks, or tasks that take a long time to complete, can be executed in the background, while the game carries on running.

What’s more, when compared to the general coroutine workflow, async in Unity can be much easier to work with, especially if all you want to do is delay part of a function or wait for a task to complete before continuing.

But is async actually better than using a coroutine?

Is it worse?

Or is it just different?

In this article, you’ll learn how async in Unity works, what it’s good for and some common pitfalls to avoid when using it so that you can decide for yourself if, and when, it’s a better option than using a coroutine.

Here’s what you’ll learn on this page:

How to use Async in Unity

Async / Await is a C# scripting feature in Unity that allows you to execute part of a function asynchronously.

Asynchronous functions work by waiting for something else, allowing you to suspend them part-way through, until a specific task has been completed.

This is similar to how coroutines are split between frames, where the function’s execution is suspended at a certain point and, when it resumes, picks up where it left off.

However, the difference is that, while coroutines typically execute logic over time deliberately, splitting up a function into stages using yield statements, an asynchronous function can be used to wait until another operation completes before doing something else.

Meaning that you’ll be able to run an expensive operation in the background, without stopping your game in the process.

So how does it all work?

To create an asynchronous function, you’ll need to add the async keyword before the method’s return type.

Like this:

async void DoSomething()
{
    // Do something!
}

However, this alone isn’t enough to create an asynchronous function.

In order to run a part of a function asynchronously, you’ll need to wait for a task to complete, using the await keyword and a Task type.

Like this:

async void DoSomething()
{
    Debug.Log("Wait for 3 seconds");
    await Task.Delay(3000);
    Debug.Log("3 seconds later");
}

A Task is a representation of an asynchronous operation.

It could be a delay, like in the example above, or a method that returns a task type, allowing an asynchronous method to wait for it.

And, when it does, it will stop what it’s doing, only continuing when the awaited task has been completed.

This is the only part of an async function that will run asynchronously, meaning that you must include an awaitable task in order to use async at all.

Awaited Task visualisation in Unity

Even an async function will be processed synchronously, just like everything else. That is, until, you await a task, which will then take place in the background.

Any code before or after the awaitable task, will happen synchronously, blocking the main thread, just like anything else would. 

Meaning that, in order to use async, you must use await, and in order to use await, you must have a Task for it to wait for.

How to create a Task in Unity

Before you can create any kind of task, you’ll need to add the System Threading Tasks namespace to the top of your script.

Like this:

using UnityEngine;
using System.Threading.Tasks;

Once you’ve done that, you’ll be able to use and create Tasks.

The Task Class represents a type of asynchronous operation.

Importantly, its progress is trackable, which is what allows an asynchronous function to await the completion of a task, without stopping everything else in the process.

But what can you do with a task?

One option is to suspend the execution of the current function by yielding it, just like you would do with a Yield Statement in a coroutine.

For example, a While Loop continues to execute until its condition is no longer true.

Like this:

void CountToTen()
{
    int number = 0;

    while (number < 10)
    {
        number++;
        Debug.Log(number);
    }

    Debug.Log("I've finished counting!");
}

Which, in a regular function, means that it happens all at once, during the same frame.

However, this can easily cause problems as, unless its contents allow it to break out of its own loop, running a while loop in a regular function can cause Unity to freeze.

This happens because Unity processes the while loop synchronously, meaning that it can’t do anything else until the loop’s condition is no longer true.

However, yielding the loop with a Task can suspend the function’s execution, allowing it to be processed over a number of frames instead of just one.

Like this:

async void CountToTenAsync()
{
    int number = 0;

    while (number < 10)
    {
        number++;
        Debug.Log(number);
        await Task.Yield();
    }

    Debug.Log("I've finished counting!");
}

Meaning that, even if your while loop contains an infinitely true condition, because it’s yielded using a task, it simply means that it will happen every frame, instead of forever in one frame.

However, what if you don’t want to wait until the next frame? What if you want to wait for an amount of time instead?

Just like with a coroutine, it’s possible to pause a function for a specific amount of time, after which the function will continue where it left off, by waiting for a Delay Task

Like this:

async void Wait()
{
    Debug.Log("Give me a second...");
    await Task.Delay(1000);
    Debug.Log("Ok, I'm done.");
}

Unlike Wait For Seconds in a coroutine, Task Delay accepts a duration in milliseconds, not seconds.

Meaning that, if you want to pass seconds into the delay parameter instead, you’ll need to multiply the duration by 1000.

Like this:

async void Wait() 
{ 
    // Waits 2.5 seconds
    await Task.Delay((int)2.5 * 1000); 
}

While yielding for the next frame, or waiting for an amount of time to pass before doing something else, are both useful, they are, typically, problems that you might solve with a coroutine.

Meaning that, apart from a difference in workflow, there’s no real benefit to using async instead of coroutines for those types of tasks.

Instead, to get the most out of async, you might find it more useful to wait for an actual function to complete instead.

So how can you do that?

One way to wait for a function to complete in the background is by awaiting an async function that returns a Task type.

Like this:

async void Start()
{
    await MyFunction();
    Debug.Log("All Done!");
}

async Task MyFunction()
{
    // Waits 5 seconds
    await Task.Delay(5000);
}

However, in order for this to work, the task that you await must, itself, await something else.

Which, if you’re doing something that returns a task type, such as delaying something for an amount of time, isn’t a problem.

But, if all you want to do is execute a block of code that could take a while to process, but that doesn’t actually await anything, then if you want to process it asynchronously, you’ll need to pass it into the Task Run method as an Anonymous Function.

Like this:

async void Start()
{
    await Task.Run(() => Count());
    Debug.Log("All Done!");
}

void Count()
{
    for (int i = 0; i < 10000; i++)
    {
        Debug.Log(i);
    }
}

This allows a, normally, synchronous block of code to run in the background.

But why is this useful?

And when would you use it?

When to use Async / Await in Unity

The main benefit of asynchronous tasks in Unity is that, unlike regular functions, they don’t block the main thread.

Which means that you can use async to deliberately perform an expensive operation in the background so that it doesn’t completely stop the game when it runs, improving the overall experience for the player.

For example, some of Unity’s built-in functions, such as Loading a Scene, or importing large assets, such as audio data, already provide asynchronous alternatives for exactly this reason.

As examples of how async can be useful, they’re good candidates for background operations because they typically take a long time to complete but it would be inappropriate to stop the game while they’re processed.

While the async / await workflow, if you have a similar task in your project, that could take a while to run, but that doesn’t necessarily need the game to be stopped while it does, allows you to do the same.

But, why stop there?

If the async / await workflow is easier to work with and, typically, doesn’t block the main thread, why not use it to completely replace coroutines?

Async vs coroutines in Unity

Asynchronous functions can be used in place of coroutines as, many of the things you might need to do with a coroutine can be done with an asynchronous function instead.

However, they work in different ways and, each, have different strengths and weaknesses.

For example, async, reportedly, doesn’t work well with WebGL builds, despite the problem supposedly being fixed in recent versions of Unity.

While one of the biggest issues with coroutines is that, unlike asynchronous functions, they can’t return data.

However, one of the biggest differences between coroutines and async / await and, in my opinion, the biggest consideration to make when choosing between the two options, is how the lifetime of the operation is handled.

For example, a coroutine exists for as long as it needs to run or for as long as the object that started it continues to exist.

Meaning that, if you destroy the object that started it, the coroutine will be stopped as well.

Asynchronous functions, however, aren’t tied to the objects that called them, meaning that they can continue to run, even after they’re destroyed.

For example, if I start a coroutine and call an asynchronous function at the same time, but then immediately destroy the object, the async function will still finish, but the coroutine won’t.

void Start()
{
    StartCoroutine(MyTask());
    MyTaskAsync();
    Destroy(gameObject);
}

async void MyTaskAsync()
{
    // This task will finish, even though it's object is destroyed
    Debug.Log("Async Task Started");
    await Task.Delay(5000);
    Debug.Log("Async Task Ended");
}

IEnumerator MyTask()
{
    // This task won't finish, it will be stopped as soon as the object is destroyed
    Debug.Log("Task Started");
    yield return new WaitForSeconds(5);
    Debug.Log("Task Ended");
}

This can be good or it can be bad, depending on what it is you’re trying to do.

For example, a coroutine that’s interrupted because its object is destroyed will never finish, meaning that any kind of decommissioning task that takes place at the end of the coroutine will never get to run.

While an asynchronous function will continue to execute, even after the object it was called on is destroyed.

Which, again, can be both helpful and unhelpful.

If an asynchronous function tries to reference its own object, even though it’s been destroyed, it will cause an error.

Missing Reference Exception in Unity - Async function

Asynchronous functions can survive beyond the lifetime of the object that called them, which can sometimes cause problems.

Normally, this can’t happen since a script and its functions are, typically, disabled with the object.

While asynchronous functions are not.

In fact, even if you load an entirely new scene, the function will still try to finish.

As a result, if there’s a chance you won’t need your async function after you’ve started it, you may need to cancel it yourself, manually.

How to cancel an async function in Unity

While simply yielding, or delaying an asynchronous task can be much simpler than when doing it with a coroutine, canceling it can be a little more complex.

This is because, in order to cancel an async function, or at least the part of the function that is actually asynchronous, the Task, you’ll need a Cancellation Token.

This works by declaring a Cancellation Token Source, which can be used to provide a cancellation token to the task when it’s run.

Then, if the object is destroyed, it can use the cancellation token source to also cancel the task that its token was passed to.

Like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;
using System.Threading;

public class AsyncExample : MonoBehaviour
{
    CancellationTokenSource cancellationTokenSource;

    void Start()
    {
        StartCoroutine(MyTask());
        MyTaskAsync();
        Destroy(gameObject);
    }

    private void OnDestroy()
    {
        cancellationTokenSource?.Cancel();
    }

    async void MyTaskAsync()
    {
        cancellationTokenSource = new CancellationTokenSource();
        Debug.Log("Async Task Started on: " + gameObject);
        await Task.Delay(5000, cancellationTokenSource.Token);
        Debug.Log("Async Task Ended on: " + gameObject);
    }

    IEnumerator MyTask()
    {
        Debug.Log("Task Started");
        yield return new WaitForSeconds(5);
        Debug.Log("Task Ended on: " + gameObject);
    }
}

However, while this will cancel the task, it will also cause a Task Cancelled Exception when it does.

Cancelled Task Exception

Canceling a Task will throw an error, which you’ll need to catch.

To fix this, you’ll need to use a Try / Catch statement.

A try / catch statement works in a similar way to an if / else statement except that it works with exception errors, not conditions.

The function will attempt to process the Try Block and, if an exception is thrown, depending on the type of exception, the Catch Block is processed instead.

This allows the task to return out of the function if an exception is caught, instead of trying to continue.

Like this:

async void MyTaskAsync()
{
    cancellationTokenSource = new CancellationTokenSource();
    Debug.Log("Async Task Started on: " + gameObject);
    try
    {
        await Task.Delay(5000, cancellationTokenSource.Token);
    }
    catch
    {
        Debug.Log("Task was cancelled!");
        return;
    }
    finally
    {
        cancellationTokenSource.Dispose();
        cancellationTokenSource = null;
    }
    Debug.Log("Async Task Ended on: " + gameObject);
}

Lastly, the Finally Block is always processed, either after the task is run successfully or after it fails, and can be useful for cleaning up after the function.

In this case, by disposing of the Cancellation Token Source, which can be important for avoiding memory leaks.

While none of this is particularly difficult, it does make async functions a little more complicated to use than they may have seemed at first.

So are they still worth it?

Should you be using coroutines or is async /await better?

Should you use Coroutines or Async / Await?

Generally speaking, because they work in different ways, choosing between coroutines and async / await isn’t always as simple as choosing the framework you prefer.

For example, coroutines are more suitable for fire and forget tasks that don’t need careful management.

While processing an intensive task in the background, so that it doesn’t stall your game while it runs, can only really be done using async.

Coroutines can sometimes be fiddly to work with, but, async functions, while they may seem simple at first, can become much more complicated as soon as you need to cancel a running task.

But, realistically, you may need to use both methods in your project at some point.

In which case, in my opinion, it’s generally easier to use coroutines for object-related game logic, and to save async for when it makes the most sense, such as when executing a long-running task in the background.

Now it’s your turn

Now I want to hear from you.

How are you using async in your project?

Do you find it easier or harder to use than coroutines?

And what have you learned about asynchronous functions in Unity that you know someone else would find useful?

Whatever it is, let me know by leaving a comment.

John French profile picture

by John French

Game audio professional and a keen amateur developer.

How was this content created

This article was written using first-hand research and experience, without the use of AI. For more information on how Game Dev Beginner articles are written, see my writing policy.

Get Game Development Tips, Straight to Your inbox

Get helpful tips & tricks and master game development basics the easy way, with deep-dive tutorials and guides.

My favourite time-saving Unity assets

Rewired (the best input management system)

Rewired is an input management asset that extends Unity's default input system, the Input Manager, adding much needed improvements and support for modern devices. Put simply, it's much more advanced than the default Input Manager and more reliable than Unity's new Input System. When I tested both systems, I found Rewired to be surprisingly easy to use and fully featured, so I can understand why everyone loves it.

DOTween Pro (should be built into Unity)

An asset so useful, it should already be built into Unity. Except it's not. DOTween Pro is an animation and timing tool that allows you to animate anything in Unity. You can move, fade, scale, rotate without writing Coroutines or Lerp functions.

Easy Save (there's no reason not to use it)

Easy Save makes managing game saves and file serialization extremely easy in Unity. So much so that, for the time it would take to build a save system, vs the cost of buying Easy Save, I don't recommend making your own save system since Easy Save already exists.

Comments

  1. This is so useful and the best async explanation I’ve found. I’ve never really delved into them but feel more confident to do so now.

    Thanks very much for writing this!

  2. I found your site recently when trying to solve a problem (can’t specifically remember what), and found your explanations very thorough and understandable, even as someone that has been using Unity as a hobby since 2011.

    I would like to know how Unity’s Jobs fit into the mutli-threading landscape, along side Coroutines and Async, if you this that is worth exploring.

  3. The way you explain it is easy to understand.
    Thanks very much for writing this!

  4. Hi John,
    I’m trying to make a async timer counter which runs in background when unity mobile game is minimised. I tried your solution but it doesn’t work when game is minimised in iPhone/Android so can you help me out with this issue?

    I know that we can use OnApplicationPause() and OnApplicationFocus() but that won’t work in my case because I’m working on multiplayer game and I want user to ping my server every second so he/she can stay in game.

    1. Author

      I don’t believe that this would work. Async works in parallel to the main thread, and sometimes on it, but it doesn’t necessarily run in the background (i.e. when the app isn’t focussed). As far as I understand it that’s not what it’s for, but this is not something I have specific experience in so a forum search might be your best bet.

  5. Thanks so much, this was very well written. One more difference between coroutine and async is that –
    Coroutine can only run with Monobehavior while,
    Async can run with/without it as its C# property

  6. Great article! Really helps understand how unity honors this fairly complicated subject.

    I’ve found async to be particularly useful when trying to chain multiple timed actions together without them having to know about each other (reduce coupling).
    await a();
    await b();

    And also useful when performing an action in a for loop where the outer loop doesn’t need to know about individual action durations.

    One minor correction:
    `(int)2.5 * 1000` equals 2000 due to operator precedence
    `(int)2.5` triggers first and yields 2
    `2 * 1000` yields 2000

Leave a Comment