Unity3D/C# - Asynchronous Programming - Coroutines vs await/async


First, let me talk a little about Asynchronous Programming. Async programming is basically to run program logic independently of the main thread. This means that if your logic running asynchronously get stuck into a loop or processing something it won't freeze the main thread.

Async programming is mainly used with multithreading. In many languages, you have native high-level ways to represent a separate thread from the main thread and send some work for it to do.

What does it have to do with Coroutines in Unity?

Well, Coroutines are a workaround that Unity made in order to allow people to easily write async code. But keep in mind, Coroutines are not multithreaded.

Now let's look a little into the Coroutine API.

public IEnumerator SomeCoroutine()
{
   //... do something
   yield return null;

   yield break;
}
Well, coroutines are basically methods that return an IEnumerator. So, in order to explain better how Unity's Coroutines work we need to take a loot into IEnumerator.

IEnumerators


An IEnumerator is the native C# implementation of the Iterable-Iterator Pattern. Again, many high-level languages have some sort of built-in signature for iterators (Java, C++, C#, etc). So, an iterator is something that will visit the elements of a container (e.g. an array) following some logic.

A simple iterator would visit and return the starting index of an array, and when the next element is requested it would move its internal counter following some logic to the next element, then visit and return the next element. It does this until there is no "next element". If you want an example of iterators, think about a 0 to (Length-1) iterator and a (Length-1) to 0 iterator. They are 2 ways to visit elements of an array following a different logic.

Now you ask me: What does this have to do with coroutines?

Well, when you have a method that uses the keyword "yield" in C# it gets compiled to an IEnumerator !!!! The compiler generates the IEnumerator class that will execute all the code until the first "yield return" statement, then it will return the element after the "return" keyword and wait until the next element is called. When the next element is called, it will execute the code until finding the next "yield return" statement and will keep doing this until it finds either "yield break" or there is nothing else.

So, every time you declare a method that returns an IEnumerator, it won't be something cool like executing in a separate thread or the program stops executing the code at the "yield" keyword. It will simply automatically get compiled to a class and return an instance of that class. Again, note that this class can get really complex as it can change values from the class that defined the IEnumerator method and allocate heap memory for the local variables because in the generated-class they are class variables to keep the context.

Coroutines


So, Unity Coroutines are basically automatically invoking methods to MoveNext() from a compiler-generated class based on the Coroutine method you created. What it does is basically when you StartCoroutine is to save the instances of the IEnumerators somewhere in the engine and invoke the respective MoveNext().

The Unity API requires you to start coroutines inside a MonoBehaviour and it also updates all coroutine at a specific sync point (between Update and LateUpdate) https://docs.unity3d.com/Manual/ExecutionOrder.html. Also, if a MonoBehaviour is deactivated or disabled the MoveNext won't be invoked.

await/async


In C# 8.0 the keywords await/async were introduced and they were made having the exactly (or almost) same intention as coroutines in Unity. So, what does await/async do?
Let's start with async:

public async Task DoSomething()
{
//do something
}

So, methods with the async keyword need to return either void, Task or Task<T>. So, first I need to say that "async void" should be avoided (unless you are dealing with event handlers).

So, the async keyword marks a method that will have asynchronous execution. The async keyword alone does not make a method asynchronous as it has no "suspend" or "resume" point, so it will simply run as a normal method.

Now, within a method marked as async, you can have the await keyword. They await keyword specifies a point where the method should be suspended (give control back to the main thread) and it should be followed by some kind of Task. The method will resume execution when the awaited Task is complete.

public async Task DoSomething()
 {
      //do something
      await Task.Delay(1000);
      //do something else 1000ms after
 }

using Unity Coroutines the equivalent would be:

public IEnumerator DoSomething()
 {
      //do something
      yield return new WaitForSeconds(1);
      //do something else 1000ms after
 }

Now, before using await/async, let's look first at what the compiler actually does.

Similar to IEnumerator/yield, the compiler automatically generates a class for the method (for await/async it is a class in debug mode and a struct when in release mode, for more information about the difference take a look at the class vs struct post).

But this class is not an IEnumerator, it is a highly optimized state machine made solely for the purpose of executing an asynchronous task using the Awaitable-Awaiter Pattern (which is very similar to the Iterable-Iterator Pattern). If you want a more in-depth look about what exactly is generated and how the execution flow for the generated state machine, please look at https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/. To keep things simple I will not delve deep into that, but please note the important points in the conclusion that Sergey reached:

  • Async methods are very different from the synchronous methods.
  • The compiler generates a state machine per each method and moves all the logic of the original method there.
  • The generated code is highly optimized for a synchronous scenario: if all awaited tasks are completed, then the overhead of an async method is minimal.
  • If an awaited task is not completed, the logic relies on a lot of helper types to get the job done.

So, await/async were made for asynchronous Tasks in any type of C# program. They are more general than Coroutines, more optimized and not Unity-dependent. That means you don't need to depend on the UnityEngine namespace or MonoBehaviours to run your coroutines/async tasks.

To run an async method, you need to:

Task.Run(DoSomething);

Task.Run return an awaitable task, do the same way you can yield return StartCoroutine(SomeCoroutine()); you can also await Task.Run(DoSomething);.

For more info about the Task namespace please go to https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netframework-4.8.

For instance, another example of how to translate coroutines to await/async:

public IEnumerator DoSomething()
  {
       while(transform.position.x <= 10)
           yield return null;
       Debug.Log("x is greater than 10!!!");
  }

public async Task DoSomething()
  {
       while(transform.position.x <= 10)
           await Task.Yield();
       Debug.Log("x is greater than 10!!!");
  }

Now, keep in mind that the checking for the while loop in the Coroutine will execute once per game frame since all coroutines iterators MoveNext are called after MonoBehaviour Update and before LateUpdate.

Please note that await Task.Yield() will return control and resume when it is convenient for the CPU based on the SynchronizationContext of the thread. This is automatically managed by the API, so it can result in the while loop being executed many times in the same game frame or possibly skipping some frames between execution.

Also, keep in mind that since a Task is not dependent on the Unity Engine, it can still run even after you stopped playing or paused in the editor or paused the editor. To cancel a running Task without the async method being over, take a look at CancellationToken.

ps: It would be nice if we had a SynchronizationContext or a TaskScheduler for Unity3D script execution pipeline in order to allow Tasks to synchronize with the main thread at the point we want (for example between Update and LateUpdate like coroutines), but with the ECS I doubt it as it looks like Unity is going into the multithreaded solution.

Comments

Popular posts from this blog

C# - Simple Graph Interfaces and a MeshGraph

Unity/C# - Grids - Simple Grid Segment struct