-1

In my WPF application I use the async await pattern a lot to keep the UI responsive. However I noticed sometimes it freezes anyway.

I looked into it and now I understand that an async method itself still runs on the UI thread, so blocking is natural.

What I don't understand then is why the build in Async methods also freeze my UI. If I call for example await HttpClient().GetAsync(); my UI will freeze.

To keep my UI actually responsive, I need to wrap that call into Task.Run(), which will make it execute on a background thread.

I did some testing with doing actual work. No Task.Delay(). I have a simple WPF window with a loading animation, so I can clearly see when the UI freezes. Edit: Added more details to show I don't do anything crazy.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MyViewModel();
    }
}

public class MyViewModel : ObservableObject
{
    public AsyncRelayCommand ButtonCommand { get; }
    public MyViewModel()
    {
        ButtonCommand = new AsyncRelayCommand(Load);
    }

    public async Task Load()
    {
        await GetALotOfBlogs(); // blocks the first time i execute it, 2nd time seems fine
        await Task.Run(GetALotOfBlogs); // does not block ever

        await HttpRequestAsync(); // does not seem to actually block
        await Task.Run(HttpRequestAsync); // does not block

        await ReadFile(); // "laggy blocking"
        await Task.Run(ReadFile); // does not block
    }

    public async Task ReadFile()
    {
        for (int i = 0; i < 100; i++)
            await File.ReadAllBytesAsync(@"C:\Yee\SomeFile.rar");
    }
    public async Task HttpRequestAsync()
    {
        HttpResponseMessage response = await new HttpClient().GetAsync("http://slowwly.robertomurray.co.uk/delay/5000/url/http://www.google.co.uk");
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();
    }

    public Task<List<Blog>> GetALotOfBlogs()
    {
        var ctx = new BloggingContext();
        return ctx.Blogs.ToListAsync();
    }
}

XAML is simply

<StackPanel>
     <Button Command="{Binding ButtonCommand}" Content="Go"/>
     <local:LoadingSpinner />
</StackPanel>

This seems conclusive enough and not unreasonable at all. The "naked" await statements execute on the UI thread, blocking it. The Task.Run statements execute on background threads, not blocking the UI thread.

The conclusion I have to take from this is: In ViewModels wrap every call to an async method in Task.Run

I read a lot about WPF development. I have seen a good amount of sample and commercial projects. Yet I have never come across this before. At least not so clearly. That makes me seriously doubt my conclusion.

Am I missing something?

Edit:

I think I understand whats happening. Especially the ef core scenario was important to me. I noticed the program is freezing even before the Task is hit, although there is no code before.

When I click the button for the first time a lot of DLLs get loaded. I get a bunch of these messages in myoutput window 'WpfApp3.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\System.Linq.Queryable.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

Net Core seems to load these DLLs dynamically as needed. It also looks like net core looks at the what's in the method before executing and then loads the DLLs necessary. Because the dll loading takes place before any other code before the actual task method.

public async Task Button()
{
    Text = "Starting";

    await GetALotOfBlogs();

    Text = "Done";
}

Text is bound to a TextBlock. The first time I click it actually goes

  1. Load DLLs (freezing my UI, if done on the UI thread)
  2. Text = "Starting"
  3. await GetALotOfBlogs();
  4. Text = "Done"

Which is interesting, because to execute Text = "Starting" it obviously does not need all those DLLs.

This DLL loading process is the one that was blocking my UI. The actual ToListAsync Task does not block. The 2nd button click executes just fine, no DLLs to load anymore after all.

Zuldaan
  • 29
  • 4
  • How do you test it? – Wiktor Zychla Nov 21 '20 at 13:03
  • 2
    @Andy: you definitely do not need threads to not to block while retrieving data. Wrapping async I/O calls with extra `Task.Run` is an antipattern, `Task.Run` should only be used [for CPU intensive tasks](https://stackoverflow.com/a/18015586/941240). My guess is that OP could be calling `....Result` at the end of their test, effectively blocking the execution at this point. – Wiktor Zychla Nov 21 '20 at 13:08
  • 1
    Which AsyncRelayCommand is it? What if you call `await ((MyViewModel)DataContext).Load();` in an async Click handler instead of a command? – Clemens Nov 21 '20 at 13:35
  • AsyncRelayCommand is an implementation from Microsoft.Toolkit.MVVM – Zuldaan Nov 21 '20 at 13:43
  • Well it depends what he's doing. Now i'm more awake and look at that code again i don't like the look of that new httpclient(). Newing up a httpclient every call is a bad idea. It's intended to be instantiated once. Except there are potential issues making a singleton and Ihttpclientfactory might be advisable.https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests – Andy Nov 21 '20 at 13:44
  • I can't reproduce any blocking for the file and http cases. Creating the HttpClient blocks for a fraction of a second, but its async methods don't. Used a ProgressBar instead of your LoadingSpinner. – Clemens Nov 21 '20 at 14:15
  • You should test this while your IDE is in Release mode (solution configurations). Debug mode is not always the best choice to test performance issues. The async code (native framework code) is very likely not the cause. As mentioned before by others, pure async methods like any I/O related API should not be executed on a background thread. Basically the whole trick of async is to execute I/O bound work load non-blocking on the UI thread (without sacrificing background thread resources). – BionicCode Nov 21 '20 at 16:22

1 Answers1

1

I can tell from experience that your conclusion is wrong. If the method you are awaiting for is truly asynchronous, there is no point in wrapping it in Task.Run. It kind of missing the point of async / await idea.

How exactly do you know that the method you are awaiting on blocks the UI?

I would recommend timing a the synchronous part of the async method to be sure:

void CallAsyncMethodDoNotAwaitInThisScope()
{
    // start timing code

    // invoke an async method, but do not await on this scope
    // this is how you will measure the synchronous part of the async method
    YourAsyncMethodThatCanPerformAwaitInternally(); // no await here

    // stop timing - measured time should be very short
}

Also, I would separately time the code after the 'await' statement to make sure it is not the culprit, As it is running on the UI thread.

If the synchronous part of the method you are invoking takes too long, then it makes sense wrapping it in Task.Run. But this usually means that the asynchronous aspect is not implemented correctly.

Elad Maimoni
  • 2,731
  • 2
  • 17
  • 27
  • Thats why I used the build in async methods. Look at especially the last one. It is just wrapping entity framework cores ToListAsync() Task. I would assume that all of those async methods are implemented correctly, yet they STILL block. – Zuldaan Nov 21 '20 at 13:11
  • The question is how do you conclude they are the ones blocking the UI. You need to provide a reproducible example. – Elad Maimoni Nov 21 '20 at 13:14
  • I comment out 5 of the 6 async lines. Press the button and look at the spinner. Since people were doubting my method i just checked with the performance profiler and the fps drops to 0 at the same point. – Zuldaan Nov 21 '20 at 13:30
  • That is not a proper test as it does not tell you which part of the code is responsible for blocking the UI thread. – Elad Maimoni Nov 21 '20 at 13:46
  • I don't understand. I care most about the ef core test and its the one performing the worst. What *other code* could possibly be the one that does the blocking? That is my entire program. its 3 lines of code. – Zuldaan Nov 21 '20 at 13:48