Async code execution in Unity

With the update to Unity 2017, came the long awaited Mono/.NET 4.6 runtime and C# 6 support.

What that means for developers is that the ability to run tasks and make async methods is finally here. In Unity today, code execution happens on the main thread. This is a bad approach because code which could have been run async have to be executed before rendering the frame can start, unless another thread is used.

IMG_4403_2.jpg

Let's look at how to improve the code flow with tasks and async methods.


First, let's look at how to execute an async method.

async void RunAsync() 
{
    await RunFirstTaskAsync();
    await RunSecondTaskAsync();
}


The purpose of async methods is to await them later, or not at all if we don't care about the return value. Async methods will still run on the main thread, but will not block the main thread unless the await keywork is used.

Tasks will on the other hand not run on the main thread, but are queued using an available thread on the threadpool.

Task.run(() => 
{
    // Code to be executed using the threadpool
});



What does this mean for Unity?

With the current implementation of UnityEngine, we're unable to gain any performance increase running UnityEngine calls async, due to them getting queued to be executed before rendering of the current frame.

void Start() 
{
    for (int i = 0; i < 10; i++)
    {
        GameObject.Instantiate(this.gameObject);
    }
}


However if any code unrelated to UnityEngine is executed, we can start to see some significant performance gains.

void Start()
{
    for (int i = 0; i < 10; i++)
    {
        InstantiateAsync();
    }
    // Method will return almost instantly, ready to do more work.
}

async void InstantiateAsync()
{
    // Example of long running code.
    await Task.Delay(1000);
    GameObject.Instantiate(this.gameObject);
}


This will still run on the main thread, but is non-blocking. We can gain even more performance by running tasks.

void Start()
{
    for (int i = 0; i < 10; i++)
    {
        Task.Run(async () => {
            // Example of long running code.
            await Task.Delay(1000);
            GameObject.Instantiate(this.gameObject);
        });
    }
    // Method will return almost instantly, ready to do more work.
}


This will seem to be the logical way to use tasks, but Unity throws an exception if UnityEngine calls are made outside the main thread. To make this work, we have to use a dispatcher that can run the UnityEngine calls on the main thread.

using System;
using System.Collections.Generic;
using UnityEngine;

public class Dispatcher : MonoBehaviour
{
    private static Dispatcher instance;

    private List<Action> pending = new List<Action>();

    public static Dispatcher Instance
    {
        get
        {
            return instance;
        }
    }

    public void Invoke(Action fn)
    {
        lock (this.pending)
        {
            this.pending.Add(fn);
        }
    }

    private void InvokePending()
    {
        lock (this.pending)
        {
            foreach (Action action in this.pending)
            {
                action();
            }

            this.pending.Clear();
        }
    }

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(this.gameObject);
        }
        else
        {
            instance = this;
        }
    }

    private void Update()
    {
        this.InvokePending();
    }
}


The dispatcher is a singleton, exposing a public method Invoke, which takes an Action as an argument. The action is put into a list of actions to be executed in the Update method. The dispatcher is a MonoBehaviour, which requires it to be put on a GameObject.

The way to run tasks will be:

void Start()
{
    for (int i = 0; i < 10; i++)
    {
        Task.Run(async () => {
            // Example of long running code.
            await Task.Delay(1000);
            Dispatcher.Instance.Invoke(() => {
                GameObject.Instantiate(this.gameObject);
            });
        });
    }
    // Method will return almost instantly, ready to do more work.
}


Keep in mind tasks will fail silently, so catching exceptions are required to get any form of error messages.