Confused about await/async within Button onClick listener

As I understand the onclick listeners are supposed to run in the main thread, so if I await them, it should block the UI until it’s done. I have an onclick listener attached to a button called “load” (I’ve added some sleep commands for debugging purposes):

    public async void OnLoadClicked() {
        Thread.Sleep(3000);

        await Task.Delay(3000);
        Debug.Log("Load clicked1.");
        await loadGameModal.PresentSelf();
        Debug.Log("Load clicked2.");
    }

What I expected to happen: The entire screen should freeze and not allow me to do anything until the whole thing is done. I thought the button listener runs in the main thread in Unity, so if I await something inside them, it should block the UI.

What actually happens: The entire screen only freezes for the first 3 seconds. Then, the menu is interactable and I can even click on other buttons. 3 seconds later I see “Load clicked1”. 1 second later I see “load clicked2” as the screen appears.

The point of async code is generally to not block the main thread. Hence, the name: asynchronous.

async void methods are fire-and-forget. They’re meant to be fired off and allowed to run in the background until they’re done. I don’t think they’re suitable for whatever you’re trying to do.

What are you trying to do exactly? Stop the UI from being interacted with? Just have a canvas group above everything so you can turn off its interactability.

with async await, a state machine is generated when compiling, which is executed with every run (I think in unity it is every frame).
Basically, in each frame it is checked whether there are waiting tasks. If the waiting task is finished, the method is continued at that point, otherwise it skips the task and continues with the rest.

Calling an async method won’t necessarily run on another thread unless told to. The await doesn’t freeze the thread either, it just relinquishes work and will pick back up later once the thing it’s awaiting completes what it’s doing. The thing it awaits may do work on another thread… but that doesn’t mean the thread your async function is running on will freeze during that time. It just means it stops processing that specific block of code until later.

async != threading

The reason you see it lock up for the first 3 seconds is because you called ‘Sleep’. This will actually lock up the thread you’re on. That’s literally what ‘Sleep’ does:

The await’s on the other hand… you can think of them similarly to how a ‘yield return’ in a Coroutine works… sort of.

The entire point of asynchronous logic is that it doesn’t freeze your program!

Whatever issue you are trying to solve, freezing the main thread is not the solution.
Rather post how you want the program to behave, and then people can make suggestions

Like if you want to block the UI while something is loading, you can put an invisible panel that covers the UI and catches all the clicks and just ignores them… or you could hide the UI etc

This post isn’t just for a pragmatic solution (and I appreciate the tips for that), but also to improve my understanding of await/async.

Am I correct in my new understanding, that some implementations of await/async would indeed block the main thread and some wouldn’t?

Say for example in OnLoadClicked() I awaited an async method called Test.

If Test consists of Thread.Sleep(3000); it will block
If Test consists of await Task.Delay(3000) it won’t block
If Test consists of await Task.Run(() => { Thread.Sleep(3000); }); it will block
If Test consists of await Task.Run(() => { await Task.Delay(3000); }); it won’t block

Is this correct?

only the Sleep will block, none of the others will.

Well technically this:

await Task.Run(() => { Thread.Sleep(3000); });

The thread that the sleep happens on will block… but that thread is not what is 'await’ing. The await will NOT block.

I see, thanks. I once had a test by putting an await in an Update. When it was unresponsive I assumed it blocked the main thread. But now as I understand, probably it was only blocking that specific Update function, and other objects’ Updates were still running

Most of Unity is single-threaded. If you locked up the Update function of one game object, you’re holding up most of the entire Unity application.

Then I’m confused again. Didn’t you say the await in OnLoadClicked() won’t block the main thread? It will “yield” and come back later? OnLoadClicked() and Update() both run in the main thread, right?

Because that’s what await/async does. It allows you to write code that stops operating, without blocking, and return to it after whatever it’s awaiting is complete.

Here… read this:

Only the first Thread.Sleep blocks the main thread (if the method is also called via the main thread) since no await is used here.

Edit:
You can also make an Update() async, but the code is continued in another frame after the awaits, you have to take that into account. and in the meantime there will be more updates waiting, so be careful with that. There can also be lags because of the contexts switch, since unity ensures that you are back in the main thread after the await. The normal behavior with C# is actually that the method is continued on the thread on which the last task was running (Unity, WPF and other UI applications are not)

an oversimplication in parts, but it’s useful to think of things in this way to get your head around it.

I’m hoping you understand coroutines…

Coroutines are Asyncronous functions.
Async functions are also Asyncronous functions.

The goal of an asyncronous function is to be able to split a function into pieces and run these pieces over time so as not to block the execution of the program (typically they are running in your main thread, and you want them to not block the main thread).

Unity Coroutines use Yield to yield control
C# Async functions use Await to yield control

The Yield or the Await will specify when exactly they want control back… perhaps next frame, perhaps when a function has completed running, or a period of time has passed. When this condition is met, they are scheduled to continue execution and are given back control.

Tasks are multithreading, async await on its own is not.

Tasks are running in different threads (not always but you should think of them as if they are), and when you await them, you yield control while waiting for them to finish what they are doing on that thread before your async await code gets scheduled to continue it’s flow of execution.

Yes it will.
Telling a Thread to sleep freezes whatever thread you are in for the specified amount of time, and you’re in the main thread, because your async function is running in the main thread.

Right, you’re awaiting it, which means you are yielding control for 3 seconds (until it is finished), so the main thread continues to run until those 3 seconds are up and the async function gets control back.

This is just a quirk of anonymous functions, it’s called Capture I think.
Your anonymous function is capturing the main thread because the function it is declared in is being run on the main thread.

If you simply Task.Run( ADedicatedFunction), which has a Thread.Sleep in it, it would not block the main thread, it would block the thread that it is running in, which would be a new thread.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class TestAsync : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        CountToTenAsync();
    }

    private async void CountToTenAsync()
    {
        Debug.Log("Starting...");
        for (int i = 1; i <= 10; i++)
        {
            await Task.Run(OneSecondSleep);
            Debug.Log(i);
        }
        Debug.Log("Finished...");
    }

    private void OneSecondSleep()
    {
        Thread.Sleep(1000);
    }
}

I’m still unable to resolve this discrepancy: On one hand Unity is single-threaded so someone said if I await in an Update and noticed it was blocking, it means the entire application was blocked. On the other hand if I put the same await in OnLoadClicked (which is also running in the single main thread), it won’t block the whole main thread. How can both of them be right?

Awaiting an async Update doesn’t block

After some testing, I wouldn’t use async Update though. Update is called automatically every frame. It doesn’t matter if your previous update is waiting to be continued. It will start a new one every frame anyway, while it’s waiting to continue your old one, and you end up with loads of them.

Create a dedicated async function instead, and call it only when it needs to be called.

I was referring to specifically this part:

public async void OnLoadClicked()
{
        Thread.Sleep(3000);

Where you’re not awaiting anything, and holding up the main thread. Doing the same in Update will hold up the whole application.

I didn’t say ‘if you await in update’, I said ‘if you lock up the Update function’. Bit of a difference.

Only the main thread to be exact. jobs, renderthread, C# tasks on other threads keep running as long as they have something to do. But it blocks everything that is running on the main thread.
A lot of things have to run on the main thread in Unity, but I think a lot has to do with restrictions on some platforms/API where access to certain APIs can only run on the same thread.

One of the issues with Async Await that you will run into when using it in unity is that, unlike unity coroutines, they don’t get stopped automatically when exiting playmode in the editor. You have to use cancellation tokens to shut them down.
This is also how shutting down tasks works, and because async await is somewhat married to tasks, that workflow filters back to them.

In the code I previously posted, if you hit playmode and then at 5 seconds exit playmode, it will keep running and counting till it hits 10. You can tell it to stop running using the unity destroyCancellationToken that gets flagged when a monobehaviour is destroyed, which also happens when exiting playmode, though this exists for monobehaviours only.

    private async void CountToTenAsync()
    {
        Debug.Log("Starting...");
        for (int i = 1; i <= 10; i++)
        {
            if (destroyCancellationToken.IsCancellationRequested) return;
            await Task.Run(OneSecondSleep);
            Debug.Log(i);
        }
        Debug.Log("Finished...");
    }

Alternatively you can create your own cancellation tokens and flag them when something happens, like a user clicks the cancel button while downloading something etc.

This is important because if you are creating or destroying gameobjects inside your async code, for example, and they are still running when you get into editor mode, you will inadvertently create and destroy gameobjects in your scene.

Another thing you will want to know about async await is that, unlike coroutines, they don’t conform to the timescale set in unity. If you tell something to wait 1 second in a coroutine and then halve the timescale, it will wait 2 seconds, so it will slow down along with the game, although it also has a realtime option. Task delay will not conform to unity timescale, it will always be realtime, because it has nothing to do with unity. Just something to keep in mind

Unity has recently implemented something called an Awaitable in newer versions that supposedly allows you await coroutine-like unity conditions like next frame, etc. I don’t know if it’s WaitForSeconds is scaled as I’m on an older version. They don’t have a realtime option, so probably not scaled.

Unity - Scripting API: Awaitable (unity3d.com)

And if you don’t want to wait for 2023 for the above feature, there is UniTask which a package that properly provides async support in Unity.