6

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();
Daniel Mann
  • 57,011
  • 13
  • 100
  • 120
Evorlor
  • 7,263
  • 17
  • 70
  • 141
  • You have a deadlock. Is this a GUI application? ASP.net? Is `PrintNumberWithAwait` an eventhandler? If not, change the `void` to be `Task` – JohanP Jan 10 '22 at 23:37
  • @JonahP I scribbled it together in Unity, but no GUI. – Evorlor Jan 10 '22 at 23:39
  • 1
    Here is a working snippet of this code: https://dotnetfiddle.net/cAvItB (I don't change anything. just put it in an async main. note that OP is using Unity. Maybe we have a framework issue.) – aloisdg Jan 10 '22 at 23:53
  • @aloisdg well now im extra confused...Result seems to be working there... – Evorlor Jan 10 '22 at 23:54
  • 1
    @Evorlor can you edit your question to add your context? You are using Unity, but which version of .net are you on? Can you provide a better [mcve]? – aloisdg Jan 10 '22 at 23:56
  • It turns out Unity is doing something wonky and doesn't like `Result`. Thank you – Evorlor Jan 10 '22 at 23:58
  • 1
    Unity has a `SynchronizationContext` which means calling `.Result` on it will cause a deadlock. If you do `GetNumber().ConfigureAwait(false).Result` it will most likely work (but you shouldn't ever do it!) – JohanP Jan 11 '22 at 00:03
  • 1
    @aloisdg if you put it inside an `async void main` ... You just shifted the issue somewhere else but again have an `async void` at the root ... – derHugo Jan 11 '22 at 07:47
  • @derHugo my snippet use `static async Task Main` – aloisdg Jan 11 '22 at 12:41
  • 1
    @aloisdg still .. this just shifts the issue somewhere else and in Unity you don't have a main method anyway ;) – derHugo Jan 11 '22 at 12:59

3 Answers3

8

Short Version

Unity's Synchronization Context is single threaded. So:

  1. Result hangs until the task completes
  2. The task cannot complete because continuation is pushed on Main Thread, that is waiting

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().

  1. [Main Thread] From PrintNumberWithResult() you call GetNumber()
  2. [Main Thread] You execute await Task.Delay()
  3. [Main Thread] The code under the await line (the return 5) is "pushed" into a "List of code" to execute after that the Task (The Delay) is completed. Small insight: This sort of list of "continuations code" is handled by a class called SynchronizationContext (It is a c# thing, not a Unity thing). You can think this class as the guy that says HOW and WHEN the code between awaits are called. The standard .NET implementation uses a thread pool (so a set of threads) that will execute the code after the task is completed. It's like an "advanced callback". Now in Unity this is different. They implemented a custom Synchronization Context that ensures all the code is executed ALWAYS in the MAIN THREAD. We can continue now
  4. [MAIN THREAD] The Delay Task is not completed yet, so you have an early return in the PrintNumberWithResult method, and you execute the .Result, that makes the Main Thread hangs there until the Task is completed.
  5. [Deadlock]. 2 things are happening in this point. The Main Thread is waiting for the Task to complete. The Custom Unity's synchronization Context pushed the code above the await to be executed in the Main Thread. But The Main Thread is waiting! So it will never be free to execute that code.

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).

4

You misunderstood what is meant by the async void that is to be avoided.

It doesn't mean you should never use a task with no result attached. It just says the asynchronous methods that invoke them should return a Task, not void.

Simply take the signature of your async method from

public async void PrintNumberWithAwait()

and replace void with Task

public async Task PrintNumberWithAwait()
{
    int number = await GetNumber();
    Debug.Log(number); //Successfully prints "5"
}

Now calling methods have the option of awaiting the result, when and if they choose to. Either:

await PrintNumberWithAwait();

Or

Task t = PrintNumberWithAwait();
// Do other stuff
// ...
await t;
Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205
  • PrintNumberWithAwait works just fine. It is PrintNumberWithResult that is locking up. – Evorlor Jan 10 '22 at 23:45
  • @Evorlor Getting `.Result` will block until the operation completes, so, yes, it will lock up. To prevent that, you need to mark the method with `async` and return some kind of `Task`. However that also means that whatever is calling your method must do the same. – Vilx- Jan 10 '22 at 23:46
  • 1
    @Everlor - the point I'm making is that there is no reason to write `PrintNumberWithResult` in the first place, because your initial statement about avoiding `async void` is misconstrued. – Andrew Shepherd Jan 10 '22 at 23:49
  • Eh I think I am being unclear in my confusion. I don't understand why PrintNumberWithResult blocks (forever...not just 1000ms) – Evorlor Jan 10 '22 at 23:51
  • @Evorlor When possible: 1) Return `Task` not `void` 2) don't rely on `.Result`. Bonus: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ – aloisdg Jan 10 '22 at 23:51
  • I typically do return Task on async methods...I actually just kinda forgot here, got confused with result. So I should never use `.Result`? – Evorlor Jan 10 '22 at 23:52
  • @Evorlor clarify in by editing your question not in comment. – aloisdg Jan 10 '22 at 23:52
  • Turns out my issue was specific to Unity, which is probably why my question didn't make a whole lot of sense. – Evorlor Jan 10 '22 at 23:59
  • 2
    In both your shown examples the thing that actually uses and calls `await PrintNumberWithAwait();` or `await t;` again needs to by `async` ... so basically this just shifts the question one level higher ... – derHugo Jan 11 '22 at 15:23
  • @derHugo Which is what happens if I return Task...so I just end up returning void anyways. I don't know of a different way to do it. – Evorlor Jan 12 '22 at 18:08
4

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.

enter image description here


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;
}

enter image description here

derHugo
  • 83,094
  • 9
  • 75
  • 115