This answer is pretty complete and explains the "issue"/subject very well.
But to extend it a little, your example with the async void
"works" for one reason: For Debug.Log
the synchronization context doesn't matter. You can safely Debug.Log
from different threads and background tasks and Unity handles them in the console. BUT as soon as you would try to use anything from the Unity API that is only allowed on the main thread (basically everything that immediately depends on or changes the scene content) it might break since it is not guaranteed that await
ends up on the main thread.
However, what now?
Nothing really speaks against using Thread
and Task.Run
in Unity!
You only have to make sure to dispatch any results back into the Unity main thread.
So I just wanted to give some actual examples of how this can be done.
Something often used is a so called "Main thread dispatcher" .. basically just a ConcurrentQueue
which allows you to Enqueue
callback Action
s from just any thread and then TryDequeue
and invoke these in an Update
routine on the Unity main thread.
This looks somewhat like e.g.
/// <summary>
/// A simple dispatcher behaviour for passing any multi-threading action into the next Update call in the Unity main thread
/// </summary>
public class MainThreadDispatcher : MonoBehaviour
{
/// <summary>
/// The currently existing instance of this class (singleton)
/// </summary>
private static MainThreadDispatcher _instance;
/// <summary>
/// A thread-safe queue (first-in, first-out) for dispatching actions from multiple threads into the Unity main thread
/// </summary>
private readonly ConcurrentQueue<Action> actions = new ConcurrentQueue<Action>();
/// <summary>
/// Public read-only property to access the instance
/// </summary>
public static MainThreadDispatcher Instance => _instance;
private void Awake ()
{
// Ensure singleton
if(_instance && _instance != this)
{
Destroy (gameObject);
return;
}
_instance = this;
// Keep when the scene changes
// sometimes you might not want that though
DontDestroyOnLoad (gameObject);
}
private void Update ()
{
// In the main thread work through all dispatched callbacks and invoke them
while(actions.TryDequeue(out var action))
{
action?.Invoke();
}
}
/// <summary>
/// Dispatch an action into the next <see cref="Update"/> call in the Unity main thread
/// </summary>
/// <param name="action">The action to execute in the next <see cref="Update"/> call</param>
public void DoInNextUpdate(Action action)
{
// Simply append the action thread-safe so it is scheduled for the next Update call
actions.Enqueue(action);
}
}
Of course you need this attached to an object in your scene, then from anywhere you could use e.g.
public void DoSomethingAsync()
{
// Run SomethingWrapper async and pass in a callback what to do with the result
Task.Run(async () => await SomethingWrapper(result =>
{
// since the SomethingWrapper forwards this callback to the MainThreadDispatcher
// this will be executed on the Unity main thread in the next Update frame
new GameObject(result.ToString());
}));
}
private async Task<int> Something()
{
await Task.Delay(3000);
return 42;
}
private async Task SomethingWrapper (Action<int> handleResult)
{
// cleanly await the result
var number = await Something ();
// Then dispatch given callback into the main thread
MainThreadDispatcher.Instance.DoInNextUpdate(() =>
{
handleResult?.Invoke(number);
});
}
This makes sense if you have a lot of asynchronous stuff going on and want to dispatch them all at some point back to the main thread.

Another possibility is using Coroutines. A Coroutine is basically a bit like a temporary Update
method (the MoveNext
of the IEnumerator
is called once a frame by default) so you can just repeatedly check if your task is done already on the main thread. This is what Unity uses themselves e.g. for UnityWebRequest
and could looked somewhat like
public void DoSomethingAsync()
{
// For this this needs to be a MonoBehaviour of course
StartCorouine (SomethingRoutine ());
}
private IEnumerator SomethingRoutine()
{
// Start the task async but don't wait for the result here
var task = Task.Run(Something);
// Rather wait this way
// Basically each frame check if the task is still runnning
while(!task.IsCompleted)
{
// Use the default yield which basically "pauses" the routine here
// allows Unity to execute the rest of the frame
// and continues from here in the next frame
yield return null;
}
// Safety check if the task actually finished or is canceled or faulty
if(task.IsCompletedSuccessfully)
{
// Since we already waited until the task is finished
// This time Result will not freeze the app
var number = task.Result;
new GameObject (number.ToString());
}
else
{
Debug.LogWarning("Task failed or canceled");
}
}
private async Task<int> Something ()
{
await Task.Delay(3000);
return 42;
}
