Note: Turns out this issue is specific to Unity.
I read that async void was to be avoided. I am trying to do so using Result, but my application keeps locking up. How can I avoid using async void?
public async void PrintNumberWithAwait()
{
int number = await GetNumber();
Debug.Log(number); //Successfully prints "5"
}
public void PrintNumberWithResult()
{
int number = GetNumber().Result;
Debug.Log(number); //Application Freezes
}
private async Task<int> GetNumber()
{
await Task.Delay(1000);
return 5;
}
I thought this was correct, but I must be missing something. How do I use async/await without having async void?
I ran my tests separately with the following code (commented one out at a time):
PrintNumberWithAwait();
PrintNumberWithResult();
Short Version
Unity's Synchronization Context is single threaded. So:
Detailed Version
You said you are using Unity. Unity is "99%" a single-threaded framework. So I suppose this code is executed on the Main, UI Thread.
Let's step into what your code do in details, when executing PrintNumberWithResult().
Solution Never call .Result.
If you want to fire and forget a task operation use Task.Run(() => ). But Wait! that's not a good idea in Unity! (Maybe it is in other .NET applications).
If you use Task.Run() in Unity you are forcing the async code to be executed using the default .NET TaskScheduler, that uses the thread pool, and that could cause some synchronization issues if you are calling some Unity related API.
What you want to do in that case is to use an async void (not really for reasons related to exception handling), an async Task method that you will never await (better), or maybe use a library as UniTask for async await using Unity (The best in my opinion).
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 Actions 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;
}
// basically same as
//yield return new WaitUntil(()=>task.IsCompleted);
// 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;
}

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With