0

I have a WPF appplication using Prism and I have found a extrage behaviour with Async method.

I have a class with async methods like this

public class ConfigurationManager(){
    public async Task<IList<T>> LoadConfigurationAsync<T>(){
        Tuple<IList<T>, bool> tuple = await LoadConfigurationItemsAsync();
        return tuple.Item1;
    }

    private async Task<Tuple<IList<T>, bool>> LoadConfigurationItemsAsync<T>(){
        await Task.Run(()  => 
        {

        });
        return new Tuple<IList<T>, bool>(configList.list, succes);
    }
}

And I needed to call them in sync form because I need the results in constructor of ViewModelManager and I try to use Result because is one of the ways to get the result in sync way.

public class ViewModelManager{
    private readonly ConfigurationManager _configManager;

    private void LoadConfiguration(){
        var config = _configManager.LoadConfigurationAsync().Result;
    }
}

For my surprise this causes the application to get blocked in the Result call, I know that Result is blocking but not for always, the return line of the method never gets executed. I tryed to call it using Task.Run and it works

private void LoadConfiguration(){
    var config = Task.Run(() => _configManager.LoadConfigurationAsync()).Result;
}

I don't know what's going on here and why calling result gets application blocked and why using Task.Run it works. It's like calling two tasks because the method is already returning a Task.

JuanDYB
  • 590
  • 3
  • 9
  • 23
  • Does this answer your question? [Can constructors be async?](https://stackoverflow.com/questions/8145479/can-constructors-be-async) – Mohammad Aghazadeh May 24 '22 at 08:29
  • Check out [this](https://stackoverflow.com/questions/72332872/c-sharp-async-deadlock/72349824#72349824 "C# Async deadlock") recent answer by Stephen Cleary, to a similar question. – Theodor Zoulias May 24 '22 at 08:33

1 Answers1

6

Accessing .Result is always a mistake (with a small caveat around "I've already checked that it has completed", with a secondary caveat about value-tasks and single access; honestly: the "always" is more useful advice than knowing the caveats!).

At the best case, you've achieved "sync over async" and tied up a thread for no good reason, impacting scalability and perhaps impacting the thread-pool.

However, if there's a synchronization context, you can - as you've found - deadlock things. A synchronization context acts as a work manager, and in the case of WPF: means by default funnelling callbacks and awaits through the UI thread. So imagine:

  • your UI thread runs, launches something in the background, then gets to the .Result and waits - not yet returning to the main app loop
  • your background thing completes, and tries to signal a pending operation as complete, i.e. trigger the continuations that are associated with an await on that operation
  • the await logic says "oh, I saw a sync-context - I need to push work via that" and adds something to the main app-loop
    • the thing being added to the app-loop is the thing that tells the task that it is complete, so that the .Result can finish
  • boom: deadlock

Precisely because the UI thread is stuck on the .Result, the app-loop never gets to actually mark the .Result as completed. There are some ways of improving this with .ConfigureAwait(false), but the main intent of that is simply to avoid running things on the UI thread when you wanted them to run in the background; this approach shouldn't be used to prevent the deadlock, even if that happens to be a coincidental side-effect.

The "fix" here is simply: don't use .Result (or .Wait()). You need to await it, and act accordingly.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for your reply! I know the logic of async methods but sometimes I needed to run them in sync way and if it's true that calling .Result is not the best sometimes I do, but I have never seen a behaviour like this. For example in console application this doesn't cause problems. If I understand well this bahaviour is caused because I have blocked the UI thread and it has no option to mark as completed the task calling .Result. Why calling via Task.Run works, I'm also calling result there? I asked it the case here to have better comprehenssion and learn more – JuanDYB May 24 '22 at 08:47
  • @JuanDYB a console application is very different from a gui application, async-wait-wise. Stephen Cleary's [blog](https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html) is good read to get you started. – Haukinger May 24 '22 at 09:37
  • @JuanDYB "For example in console application this doesn't cause problems" - yup; console applications *don't have a sync-context*, i.e. they don't have a UI thread and application loop. That doesn't mean it was ever OK to call `.Result` - just: you *got away with doing something inadvisable*. It was still inadvisable. – Marc Gravell May 24 '22 at 13:00