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
.
CodePudding user response:
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 await
s 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
- the thing being added to the app-loop is the thing that tells the task that it is complete, so that the
- 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.