1

Let's assume that I want to run two bits of work on button click and they both take some time. First bit is not thread-safe, so it is not feasible to make it async. Second bit is awaited. I want UI to acknowledge right away that the button was clicked and for instance disable it. Is there a better approach than slamming await Task.Yield(); after isBusy = true;, or this is the way?

@using System.Threading;

<MudButton OnClick="DoSomethingAsync" Disabled="@isBusy">Do something</MudButton>

@code {
    bool isBusy;

    async Task DoSomethingAsync()
    {
        isBusy = true;
        
        Thread.Sleep(2000); // simulate sync work
        
        await Task.Delay(2000); // simulate async work
        
        isBusy = false;
    }
}

https://try.mudblazor.com/snippet/waGHOLGOHpPUkLLM

I know that ideally, all long-running operations would be asynchronous and wouldn't block the UI thread. But I want to know valid approach, if I have a synchronous operation that can't be run off the main thread and can't be converted to asynchronous task.

Pawel
  • 891
  • 1
  • 9
  • 31
  • 1
    There's no "UI thread". If you use Blazor WASM, there's only one thread per browser tab. If you want to do something in the "background" you need to use service workers. That's why everything that can block is asynchronous. In Blazor Server, you're still in a server-side web application, without any UI. HTML is sent and rendered on the browser – Panagiotis Kanavos Jul 20 '23 at 09:54
  • 2
    What kind of application are you building and what kind of long-running operation are you trying to perform? On the server ,you can use BackgroundService or libraries like HangFire and Quartz.NET. – Panagiotis Kanavos Jul 20 '23 at 09:56
  • 1
    Specify [blazor-server] or [blazor-wasm] – H H Jul 20 '23 at 10:10
  • @PanagiotisKanavos [Polite] - Not strictly true - Blazor doesn't just run on a thread, it runs in a *Synchronisation Context* - see https://learn.microsoft.com/en-us/aspnet/core/blazor/components/synchronization-context. And Blazor Server does have a pseudo UI within the Hub session. – MrC aka Shaun Curtis Jul 20 '23 at 12:24
  • Synchronization contexts aren't runnable, they're a way of managing state across actual thread executions. The docs don't say otherwise. `Context`, the word, means the state of things in which something else happens. Having a DOM model doesn't mean there's a UI thread on the server either. The server keeps using multiple threads. – Panagiotis Kanavos Jul 20 '23 at 12:25
  • The repro link points to WASM, but in my particular case, I'm using Blazor Server. The subtle differences are that in WASM, the web browser appears to freeze during a synchronous call, whereas on the server, the app seems busy, but not the browser. This doesn't bother me too much. What I want to achieve is for the Blazor to acknowledge `isBusy = true` and disable the button right after the call. When I add `await Task.Yield()`, the button behaves as I would like it to in both cases just not sure if this is valid approach. – Pawel Jul 20 '23 at 12:26
  • @Pawel WASM vs Server is a *huge* difference. Your code runs in ASP.NET Core, the same way any other ASP.NET Core code does, using threadpool threads that are *not* dedicated to a single user. The limitations around long-running jobs still apply which means you must use a BackgroundService to avoid starving the threadpool, or getting your work cancelled – Panagiotis Kanavos Jul 20 '23 at 12:28
  • @PanagiotisKanavos - [Polite] "Synchronization contexts aren't runnable", did I imply they were? I was just pointing out that Blazor uses a Synchronisation Context (a level of abstraction above a thread) which as you say manages execution on actual threads. The purpose of a Synchronisation Context is to enforce a single logical thread of execution i.e. ensure only piece of UI code is running at one time, which you don't get by invoking the code on the thread pool. – MrC aka Shaun Curtis Jul 20 '23 at 17:54

2 Answers2

1

I want UI to acknowledge right away that the button was clicked and for instance disable it. Is there a better approach than slamming await Task.Yield(); after isBusy = true;,

I prefer Task.Delay(1) over Task.Yield() but for the rest this is the best/only way to do what you want on WebAssembly.

On Blazor Server you can execute the blocking code on another Thread:

await task.Run(() => {  Thread.Sleep(2000); });

I would still put a Delay(1) after setting isBusy but it might work without that.

H H
  • 263,252
  • 30
  • 330
  • 514
  • On investigating another problem of a similar nature I put some hours into investigating how Yield() and Delay() differ in execution. The difference appears to be in when the OnAfterRender methods are run. However, the delay needs to be greater than the time it takes to run the first render (and whatever else is happening on that thread!). As that is always longer that 1ms in my experiments, then they are equivalent . There's a gist here with the details - https://gist.github.com/ShaunCurtis/e7078556bdc0fb786fa889ee2f2046e3 – MrC aka Shaun Curtis Jul 20 '23 at 17:31
  • I looked at your gist and I think the main cause of our differences here is that you did this on Blazor Server. Repeat it with client-side, you'll see that Yield() is a dud there. – H H Jul 21 '23 at 20:17
  • It's an [old issue](https://stackoverflow.com/q/23431595/60761) – H H Jul 21 '23 at 20:26
  • Interesting reading - the more you read the less you realise you know! On above, I've run the code on both now. I didn't expect to see a difference but I did. WASM always runs the *OnAfterRender* code block before the `OnInitializedAsync/OnParametersSetAsync` continuation on Yield and Delay. – MrC aka Shaun Curtis Jul 22 '23 at 11:40
0

Is there a better approach than slamming await Task.Yield(); after isBusy = true;, or this is the way?

You aren't Slamming anything. You're setting where the first yield happens, and thus the intermediate "I'm Busy" render occurs.

Here's a detailed explanation of what's happening.

Some background information:

  • All BLazor UI code [lifecycle methods, UI events an callbacks] is executed on a Synchronisation Context [SC from now on], which enforces a single logical thread of execution.

  • In ComponentBase inherited components, UI events are handled by the registered IHandleEvent.HandleEventAsync. This is a simplified version.

    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
    {
        var uiTask = item.InvokeAsync(obj);

        var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;

        if (!isCompleted)
        {
            this.StateHasChanged();
            await uiTask;
        }

        this.StateHasChanged();
    }

When the button gets clicked and the renderer handles the event.

IHandleEvent.HandleEventAsync is invoked and passed the registered handler [DoSomethingAsync].

It invokes DoSomethingAsync

        var uiTask = item.InvokeAsync(obj);

The first two lines of DoSomethingAsync are a synchronous block so run and block the SC.

        isBusy = true;
        
        Thread.Sleep(2000); // simulate sync work

After 2000 ms, we reach the await:

        await Task.Delay(2000); // simulate async work

This creates a continuation on the SC, and yields.

This code block in IHandleEvent.HandleEventAsync now runs. It queues a render request on the Renderer's queue and yields [if uiTask han't completed]. The Renderer gets control of the SC, services it's queue and renders the component.

        var isCompleted = uiTask.IsCompleted || uiTask.IsCanceled;

        if (!isCompleted)
        {
            this.StateHasChanged();
            await uiTask;

After 2000 ms:

        await Task.Delay(2000); // simulate async work

completes and the contination runs to completion.

        isBusy = false;

In IHandleEvent.HandleEventAsync

uiTask is now completes so:

       this.StateHasChanged();

runs and queues a render. The SC is free so the renderer services it's queue and renders the component.

If you're running on Blazor Server, you can offload your sync task to the threadpool. If not, then you're stuck with the SC.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31