Search code examples
c#wpfasync-awaitprism

WPF calling Result of async method causing block


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.


Solution

  • 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.