Experiments with async/await and Task to run multithreaded code

A couple of years ago when unity started supporting .NET 4.x we got access to newer C# features, including async/await. Today I decided to see if I could use this to easily run multithreaded code in unity.

First I would like to point out that asynchronous does not mean multithreaded. Just having an async method is more akin to having a coroutine method like we are used to in Unity. But in an async method we can start (and wait on) a task on a separate thread using something like this:

private void Start()
{
    Test();
}

private async void Test()
{
    string result = "";
    await Task.Run(() => {
        Thread.Sleep(1000);
        result = "This is some result";
    });
    Debug.Log(result);
}

With this in mind I created a static helper class where you can run any method on a separate thread and get the result sent back to the main thread via a callback action

using System;
using System.Threading.Tasks;

public static class RunOnSeparateThread
{
    public static async void Run<TResult>(Func<TResult> task, Action<TResult> onDone)
    {
        TResult result = default;
        await Task.Run(() => result = task());
        onDone?.Invoke(result);
    }

    public static async void Run<T1, TResult>(Func<T1, TResult> task, Action<TResult> onDone, T1 arg1)
    {
        TResult result = default;
        await Task.Run(() => result = task(arg1));
        onDone?.Invoke(result);
    }

    public static async void Run<T1, T2, TResult>(Func<T1, T2, TResult> task, Action<TResult> onDone, T1 arg1, T2 arg2)
    {
        TResult result = default;
        await Task.Run(() => result = task(arg1, arg2));
        onDone?.Invoke(result);
    }

    public static async void Run<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> task, Action<TResult> onDone, T1 arg1, T2 arg2, T3 arg3)
    {
        TResult result = default;
        await Task.Run(() => result = task(arg1, arg2, arg3));
        onDone?.Invoke(result);
    }
}

The class has methods for taking in a System.Func delegate, a callback method with the return value of the delegate, and then all the arguments for the delegate. It’s worth noting that System.Func can take up to 16 arguments, I only implemented overrides for up to 3 as I feel like that’s enough for proof of concept. I then created a little test script and attached to to a gameobject in the scene:

using System.Threading;
using UnityEngine;

public class Example : MonoBehaviour
{
    private void Start()
    {
        RunOnSeparateThread.Run(SlowTask, OnSlowTaskDone, 10, 1000);
    }

    private void Update()
    {
        //Illustrate that main thread is still running
        transform.Rotate(Vector3.up, Time.deltaTime * 10);
    }
    private void OnSlowTaskDone(string result)
    {
        LogWithThreadId(result);
    }

    private string SlowTask(int count, int wait)
    {
        for (int i = count; i >= 0; i--)
        {
            LogWithThreadId(i);
            Thread.Sleep(wait);
        }
        return "Result: 42";
    }

    private void LogWithThreadId(object content)
    {
        Debug.Log("ThreadId " + Thread.CurrentThread.ManagedThreadId + ":\t" + content);
    }
}

This script just starts a slow method that counts down and writes to the console, by the end the method returns a result that a callback method prints that result to the console. I also made the gameobject rotate in the Update method just to illustrate that the main thread was still running.

Everything seems to be working, but I want to point out that I have only tested this in the Editor on windows so no guarantee that this will work on all platforms

Leave a Reply

Your email address will not be published. Required fields are marked *