2

I am trying to create an async method in Blazor that allows me to run computationally-heavy code asynchronously and then update a component when done, all while keeping the page responsive. The difference between this question and other questions about async in Blazor, is that I am creating a method that takes a long time versus using something like HttpClient's download methods.

The components:

page.blazor

@inject ClassA classA

<button class="btn btn-primary" @onclick="@doWork">Do Work</button>
<Text>@log</Text>

@code {
   async Task doWork()
   {
      log = await classA.doWork();
   }
}

C# classes

public class ClassA
{ 
   public async Task<string> doWork()
   {
      ClassB classB = new ClassB();
      var result = await classB.Execute();
      return result;
   }
}

public class ClassB
{ 
   // This method is meant to take a long time and execute asynchronously
   public async Task<string> Execute()
   {
      string k = "";
      for (int i = 0; i < 10000000; i ++) 
      {
         k = i.ToString();
      }
      return k;
   }
}

Note: If I change the code in page.blazor to

@code {
   async Task doWork()
   {
        await Task.Delay(10000);
        log = "Done";
   }
}

it works properly.

Jason Batson
  • 21
  • 1
  • 2
  • Task.Run is what you need – pm100 Feb 27 '22 at 19:32
  • 4
    In Blazor WebAssembly this isn't going to work for the time being. For CPU work you need extra threads and you don't have them here. Task.Run() won't help much, you need a server and/or cut the work into smaller pieces. – H H Feb 27 '22 at 19:33
  • @HenkHolterman How does Task.Delay(10000) not block the UI, if this is the case? Or HttpClient's GetAsync(URL)? – Jason Batson Feb 28 '22 at 01:32
  • 2
    `Task.Delay()` is async, use it to simulate I/O. `Thread.Sleep()` is sync, you can use it to emulate CPU load. – H H Feb 28 '22 at 05:24
  • And "not blocking" is the main reason for async/await patterns. See [this](https://stackoverflow.com/a/60584976/60761) and [this](https://stackoverflow.com/a/61566992/60761) – H H Feb 28 '22 at 05:29
  • And [one more](https://stackoverflow.com/a/61865580/60761) – H H Feb 28 '22 at 05:44
  • 1
    Can you confirm that this is a Blazor WASM application? One of the tags is "Blazor-webassembly", but lets be sure because it matters! – MrC aka Shaun Curtis Feb 28 '22 at 09:35
  • 1
    Multithreads are part of the roadmap for .Net 7 Wasm.(https://github.com/dotnet/aspnetcore/issues/17730#issuecomment-1043597775). It's not in the 1st preview though. Unfortunately, beside trying the yields(or Task.Delay(very short mseconds span) every loop or something) so that you leave time for the rest to execute) in the answer under, there ain't much to do except try to optimize your code as much as you can. For now at least – Shuryno Feb 28 '22 at 13:26

2 Answers2

3

Updated Answer - see @HenkHolterman's comment.

This code block isn't correct. In Visual Studio you will get the warning shown below.

   public async Task<string> Execute()
   {
      string k = "";
      for (int i = 0; i < 10000000; i ++) 
      {
         k = i.ToString();
      }
      return k;
   }

enter image description here

It should look like this:

   public Task<string> Execute()
   {
      string k = "";
      for (int i = 0; i < 10000000; i ++) 
      {
         k = i.ToString();
      }
      return Task.FromResult(k);
   }

You don't make something Async by wrapping it in a Task and telling it to run async. Execute is a synchronous block of code wrapped in a Task. There's no yielding, it just blocks the thread until completion - just like Thread.Sleep(1000).

In Blazor Web Assembly there's ONE thread at present. You can do all the awaits you like on your code. Unless there's a true yield, there's no thread time for the UI and no updates: it all happens at the end!

On the other hand, Task.Delay(xxx) yields. In simple terms, the Task gets moved down the queue by the thread scheduler letting other code already in the queue execute.

Here's a version of your iterator that will yield.

        public async Task<string> Execute()
        {
            string k = "";
            var x = 0;
            for (int i = 0; i < 10000000; i++)
            {
                k = i.ToString();
                if (x > 100)
                {
                    await Task.Delay(1);
                    x = 0;
                }
                x++;
            }
            return k;
        }

And here's a test page:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Async Test</h1>

<div class="p-2">
    Time: @message
</div>
<div class="p-2">
    <button class="btn btn-primary" disabled=@go @onclick=ButtonClick>@buttonMessage</button>
</div>
<div class="p-2">
    <button class="btn btn-secondary" @onclick=UpdateTime>Get Time</button>
</div>


@code {
    private bool go = false;

    private string disabled = "";

    private string buttonMessage => go ? "Running" : "Go";


    private string message = DateTime.Now.ToLongTimeString();

    private void UpdateTime()
    {
        message = DateTime.Now.ToLongTimeString();
    }

    private async Task ButtonClick()
    {
        go = true;
        await this.Execute();
        go = false;
    }

    public async Task<string> Execute()
    {
        string k = "";
        var x = 0;
        for (int i = 0; i < 100000000; i++)
        {
            k = i.ToString();
            if (x > 100)
            {
                await Task.Delay(1);
                x = 0;
            }
            x++;
        }
        return k;
    }
}

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • The CPU-intensive code is run in Blazor WebAssembly only. Neither the `Task.FromResult` solution nor the `await Task.Yield()` versions resulted in a responsive UI. – Jason Batson Mar 01 '22 at 00:50
  • @JasonBatson: replace Task.Yield() with Task.Delay(1) and make sure it gets called ~10 times / second. – H H Mar 01 '22 at 08:14
  • @JasonBatson Thanks to Henk for the mods. Henk - here's one situation where Yield fails and Delay works (and is fairly obvious once you think about it). I've updated the answer to reflect the mods and added a test page. – MrC aka Shaun Curtis Mar 01 '22 at 11:17
  • I would be interested in understanding why Task.Delay works and not Yields? I have tested Yields yesterday and it worked, although it wasn't UI input, but another tasks with a Task.Delay in it. Thanks :) – Shuryno Mar 01 '22 at 13:25
  • 2
    @Shuryno. I believe it's to do with how the thread scheduler implementation prioritises it's queue. I read somewhere, though as usual I can't find it now, that in the UI context in DotNetCore you should not assume that UI stuff will get priority. With Yield you just get a re-organisation of the tasks by the scheduler. While with a Delay you get re-organisation and some free time for UI events. I may of course be totally wrong!! :-) – MrC aka Shaun Curtis Mar 01 '22 at 13:54
1

Based on https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=net-7.0

You can do:

public async Task<string> doWork() { return await Task.Run(()=> yourwork()); }

Martin VU
  • 97
  • 6