Unity Async vs Coroutine

Unity Async vs Coroutine

UPDATE (7/2/2021): As pointed out by @neuecc, one of the authors of UniTask, the stress test was invalid and the results were incorrect. Since then I've rewritten the tests and updated the graphs and conclusions under the UniTask section.

Async support was introduced on Unity 2017 with the use of the .NET 4.x runtime. Since then, not only we have unlocked the full power of asynchronous programming, but also have the choice to switch our coroutines in favor of async/await. But is it worth it?

I wrote this article in 2017 about coroutines and how to use them. Check it out to learn more!

When I first learned about async after watching this great Unite 2019 talk, I instantaneously started to introduce async programming in my projects. After a while, I had some questions and couldn’t really find anybody talking about them. On the talk I mentioned, they provide some cons and pros for both coroutine and async, but for me, it wasn’t very friendly. I wanted to know what happens on a more general level, with what I’m used to work with. If you ever felt the same about this subject, I’m here to answer your questions.

In-Depth Comparison

We’re going to consider a few things for the comparison: performance, usability, and control. I really wanted to know if there was any performance impact when using coroutines or async, while flexibility and control is something you can understand better while programming.

Performance

I created a very simple benchmark on Unity (2020.2.1f1), using the Test Tools. The first test is simply meant to measure the time spent on the yield and await instructions, used on coroutines and async respectively. As expected, there isn’t much difference from one to another:

No alt text provided for this image

The actual difference between the milliseconds time it takes to process yield and await is very small. This test has 100 samples, and you can see a very small difference between both systems. Technically what’s happening behind these lines is:

Async

await Task.Yield();

Coroutine

yield return null;

Functionally, these lines do the same thing: pause the execution until the next frame. We’re comparing here the amount of time it takes to perform the instruction and return on the next frame. So it makes sense to get the results we got.

Now let's take a look at memory. I've written a stress test to create 100 coroutines and async to compare their difference in the memory.

No alt text provided for this image

This test is measuring the total memory used by the application. While using coroutines, we can see that the total memory being used is slightly larger than with async. Doing the calculations, the average memory footprint of a coroutine is somewhat around 3~4 kB, while with async it lies somewhat over 1~2 kB. This is a small difference, barely enough reason to choose one system over another. There's more:

No alt text provided for this image

Here we can see that the async consumes more Garbage Collector Memory, going up to 40 kB, while coroutine ranges from 8 kB to 20 kB. This is, considering that we are executing 100 simultaneous async/coroutine operations. The results may vary under different environments, but the overall differences should remain the same. One last test:

No alt text provided for this image

While analyzing allocation, async allocates much more garbage than coroutines. It's 280 bytes per async and only 56 per coroutine, so it's almost 5 times more allocation. These are small values though. Garbage Collection may impact gameplay performance, but with these results, it's unlikely that they will be the issue unless you have to run hundreds of operations simultaneously.

Finally, as far as core performance is concerned, there's a small difference with async being slightly heavier to the memory than coroutines, but not to a degree you should be worried about.

Usability

Here we’re going to analyze in which situations we can use both functionalities and check which one can be used more widely and with easier implementation.

1. How does it start?

Async

void Start()
{
  WaitAndGoAsync();
}

async void WaitAndGoAsync()
{
  await Task.Delay(1000);
  Go();
}

Coroutine

void Start()
{
  StartCoroutine(WaitAndGo());
}

IEnumerator WaitAndGo()
{
  yield return new WaitForSeconds(1);
  Go();
}

Both implementations are rather simple. Although, with coroutine, you specifically need to have a MonoBehaviour to run your coroutine. To me, that’s a limiting factor, especially when working with Scriptable Objects. So, in terms of usability, async has an advantage.

2. How does it stop?

Async

void Start()
{
  CancellationTokenSource tokenSource = new CancellationTokenSource();
  WaitAndGoAsync(tokenSource.Token);
  tokenSource.Cancel();
}

async void WaitAndGoAsync(CancellationToken token = default)
{
  await Task.Delay(1000, token);
  Go();
}

Coroutine

void Start()
{
  Coroutine routine = StartCoroutine(WaitAndGo());
  StopCoroutine(routine);
}

IEnumerator WaitAndGo()
{
  yield return new WaitForSeconds(1);
  Go();
}

To stop an async method, you actually have to cancel it via a CancellationToken. It can get troublesome depending on how you have your calls and classes set up. The coroutine is fairly easier, you just have to save a reference to the Coroutine object created on start and use it to cancel. Alternatively, you can use the IEnumerator method or the name string, but here goes a friendly tip: never use string.

3. Waiting capabilities: what and how to wait?

Async

void Start()
{
  WaitAndGoAsync();
}

async void WaitAndGoAsync()
{
  // Await time delay
  await Task.Delay(1000);
  // Await custom method
  await MethodAsync();
  Go();
}

async Task MethodAsync()
{
  while (condition)
  {
    progress++;
    // Await until next frame    
    await Task.Yield();
  }
}

Coroutine

void Start()
{
    StartCoroutine(WaitAndGo());
}

IEnumerator WaitAndGo()
{
  // Wait time delay
  yield return new WaitForSeconds(1);

  // Wait time delay ignoring Time.timescale
  yield return new WaitForSecondsRealtime(1);

  // Wait until condition is met
  yield return new WaitUntil(() => active);

  // Wait while condition is met
  yield return new WaitWhile(() => active);

  // Wait until end of frame
  yield return new WaitForEndOfFrame();

  // Wait until next physics update
  yield return new WaitForFixedUpdate();

  // Wait web request/scene load
  yield return UnityWebRequest.Get("localhost:3000").SendWebRequest();
  Go();
}

Unity prepared the Coroutine system with lots of custom YieldInstructions that are directly connected to Unity’s Time system. You can yield scene loads, web requests, game frames, and time. Async awaits whatever async method. This is very flexible, but it can only await as long as you have async. The downside is that you are probably going to create some custom methods to behave closely to those Yield Instructions from Coroutines. On one side, you have many helper methods, on the other, you have the possibility to work in various different ways.

Prefer using coroutine when working directly with Unity’s Time system or Async Operations.

4. Waiting for multiple conditions

When using coroutines there’s no way to wait for multiple conditions unless you create a new Yield Instruction that does so (I have no idea how though). With async, you can use Task.WaitAny or Task.WaitAll and add multiple Tasks to control at the same time. On the first one, whatever task completes first will trigger the completion of the wait, and the other needs all tasks to be completed. This has proven to be very useful due to its flexibility.

5. Return value

Async Tasks can return values! You can perform a long-lasting operation and return a value when ready. That could be an operation such as loading a file. If you want to return a value with coroutines, you have to use a callback instead, as an Action parameter for example.

Control

How can we get info out of the running process? How much control do we have? Let’s take a look.

1. State

The only reference you have from the coroutine is the Coroutine object, which is assigned at on start and has absolutely no information if the process has ended or what is going on with it. The Task object has multiple ways of getting its state, so it’s fairly easier to manage.

2. Finalization

Regardless of the process be completed successfully or not, only the async can be guaranteed to call the finalization code. Take this example:

async Task PerformTaskAsync(CancellationToken token = default)
{
  try
  {
    await DoWorkAsync(token);
  }
  finally
  {
    DisposeObjects();
    ResetUI();
  }
}

If anything happens to the DoWorkAsync or if the task gets canceled, the finally block will always execute. This is also very useful to control UI or any other feedback. With coroutines, once you cancel its execution, it will not execute any additional code inside the IEnumerator. You can, alternatively, perform the actions on the same code you are stopping the coroutine.

3. Error handling

Coroutine errors are thrown just like any other regular Exception. Whenever an error is thrown inside an async Synchronization Context, it generates a massive error stack trace that can be quite overwhelming to debug if you’re not used to it. You can also handle exceptions in try-catch blocks or swallow them (although you shouldn’t).

Conclusion

Both systems have pros and cons, and they may perform better in different situations. Let’s see how it goes:

Table showing the conclusion comparison between Async and Coroutine

This is sort of a personal conclusion based on my daily usage and necessities. I'd suggest using a mix of both systems, basically:

Prefer Async when:

  • Requires finalization
  • Requires multiple wait conditions
  • Requires return value
  • Performing IO operations
  • Performing multi-threaded operations

Prefer Coroutine when:

  • Requires sync with Unity’s Time system or physics
  • Handling Async Operations (Scene Management/Web Request)
  • Fire and Forget

After some time you’ll quickly find out which one you will prefer for each situation. Last but not least, after understanding the core differences between both systems, I have good news. It all gets better with UniTask.

Uni Task

Imagine that Async and Coroutine had a baby, that would be UniTask. This extraordinary package provides an async/await alternative that is fully integrated to Unity, having support for awaiting AsyncOperations, MonoBehaviour/uGUI events and it gets better: it runs with Unity’s Player Loop (allowing it to work on WebGL). Basically what you get is the Async flexibility and control with all Coroutine benefits plus a performance boost since it’s lighter than the native Async (read details here). Of course, I also ran the performance tests with UniTask, take a look:

No alt text provided for this image

As we saw in the performance section, this is expected. The time it takes to run UniTask.Yield() matches the coroutine and async counterparts.

No alt text provided for this image

Here's where it gets interesting. The total memory load of UniTask is incredibly low. It can get up to 10 times lower than the total async memory.

No alt text provided for this image

Yes, this graph is correct. UniTask does not generate any GC Memory. Gameplay impact? None.

No alt text provided for this image

Here we can see that what they state in their official repository is true: UniTask is an allocation-free async/await alternative. There's much more that could be benchmarked but I think it already expresses the general impacts of each alternative.


I think that’s enough information for a single post, right? I hope you were able to understand the systems and how to use them. What do you use most on your code? Share your thoughts and if there are any questions or comments you’d like to point out. We'll meet again!

Andrei Burkhanov

Lead Unity Developer

1 年

Please , check Unitask docs. https://github.com/Cysharp/UniTask#async-void-vs-async-unitaskvoid "async Task MethodAsync()" and in this case you can return Unitask. if you will use standard C# task system, it's will be expensive)

回复
Batuhan Dev

Technical VFX Artist @My Lovely Planet

3 年

Thanks for sharing!

Lukasz (Luke) Klejnberg

Game Programmer at Rage Quit Games

3 年

Hey, great stress test! I was thinking about what it would be the result if you would cache for example WaitForSecondsRealtime to reuse without allocating every call.

Rodrigo Martins Moraes

Desenvolvedor de software

4 年

N?o uso unity a muitos anos, mas pretendo voltar, pelo oque eu entendi, o uso de Coroutine é melhor nos casos em que o tempo de execu??o do Unity é fundamental, ou seja, se é algo que deve respeitar o processamento do game, é melhor usar Coroutine, caso contrário, n?o faz muita diferen?a (para os casos testados). é isso?

要查看或添加评论,请登录

Jo?o Borks的更多文章

  • Unity Async vs Coroutine [pt-br]

    Unity Async vs Coroutine [pt-br]

    ATUALIZA??O (7/2/2021): Como apontado por @neuecc, um dos autores da UniTask, o teste era inválido e os resultados…

    7 条评论
  • Esse ano na Unity (2020)

    Esse ano na Unity (2020)

    Tradu??o do original em inglês: This year in Unity (2020) Quer você seja um amador, um profissional ou um jogador, deve…

    2 条评论
  • This year in Unity (2020)

    This year in Unity (2020)

    Whether you are a hobbyist, a professional, or a player, you must be familiar with Unity, and some of the games…

    1 条评论
  • Jack - The Circus of Illusion

    Jack - The Circus of Illusion

    This is a good one, Jack – The Circus of Illusion is the third game made by Emperium, produced in 2 months and…

  • Kitsune - 3D Adventure Demo

    Kitsune - 3D Adventure Demo

    Kitsune is the second digital game I worked, the second game made by Emperium, produced in 2 months and displayed in…

  • Recall - A 2D Platformer

    Recall - A 2D Platformer

    Recall is the first digital game I’ve ever worked, produced in 2 months by Emperium and released in June 2015. It is a…

  • About Coroutines in Unity Engine

    About Coroutines in Unity Engine

    The Unity Engine offers us, developers, a lot of rich resources and content to make our job easier. Amongst them, there…

    2 条评论
  • An Unity 2D Touch Study

    An Unity 2D Touch Study

    Version 1.0.

社区洞察

其他会员也浏览了