Home > Enterprise >  Task.Wait for async method passes during application startup while it causes deadlock in WPF button
Task.Wait for async method passes during application startup while it causes deadlock in WPF button

Time:03-29

The behavior of Task.Wait() is unexpectedly different depending on the "environment" where invoked.
Calling Task.Wait() during application startup with below async method TestAsync passes (doesn't cause a deadlock) while the same code blocks when called from within a WPF Button handler.

Steps to reproduce:
In Visual Studio, using the wizard, create a vanilla WPF .NET framework application (e.g. named WpfApp).
In the App.xaml.cs file of the app file paste below Main method and TestAsync method.
In the project properties set Startup object to WpfApp.App.
In the properties of App.xaml switch Build Action from ApplicationDefinition to Page.

    public partial class App : Application
    {
        [STAThread]
        public static int Main(string[] args)
        {
            Task<DateTime> task = App.TestAsync();
            task.Wait();

            App app = new App();
            app.InitializeComponent();
            return app.Run();
        }

        internal static async Task<DateTime> TestAsync()
        {
            DateTime completed = await Task.Run<DateTime>(() => {
                System.Threading.Thread.Sleep(3000);
                return DateTime.Now;
            });
            System.Diagnostics.Debug.WriteLine(completed);
            return completed;
        }
    }

Observe that the application starts properly (after 3sec delay) and that the "completed" DateTime is written to debug output.
Next create a Button in MainWindow.xaml with Click handler Button_Click in MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        ...

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Task<DateTime> task = App.TestAsync();
            task.Wait();
        }
    }

Observe that after clicking the Button, the application is deadlocked.

Why can't it pass in both cases?
Is there a way to change invocation (e.g. using ConfigureAwait at the correct task or somehow setting SynchronizationContext or whatever) so that it behaves identical in both invocations, but still synchronously waits for completion?

Update on limitations of the solution.

The async method like TestAsync comes from a library that cannot be changed.
The invocation code of the TestAsync method is nested within a callstack that cannot be changed either, and the code outside the callstck makes use of the returned value of the async method.
Ultimately the solution code has to convert the async method to run synchronous by not changing the method nor the caller.
This works well within UT code (NUnit) and during application startup, but no more within a handler of WPF.
Why?

CodePudding user response:

There are a couple of different ways that you can handle this situation, but ultimately the reason there is a deadlock in one situation and not the other is that when called in the Main method SynchronizationContext.Current is null, so there isn't a main UI context to capture and all async callbacks are handled on thread pool threads. When called from the button, there is a synchronization context which is captured automatically, so all async callbacks in that situation are handled on the main UI thread which is causing the deadlock. In general the only way you won't get that deadlock is by forcing the async code to not capture the synchronization context, or use async all the way up and don't synchronously wait from the main UI context.

  1. you can ConfigureAwait(false) inside of your TestAsync method so that it doesn't capture the synchronization context and try to continue on the main UI thread (this is ultimately what is causing your deadlock because you are calling task.Wait() on the UI thread which is blocking the UI thread, and you have System.Diagnostics.Debug.WriteLine(completed); that is trying to be scheduled back onto the UI thread because await automatically captures the synchronization context)
DateTime completed = await Task.Run<DateTime>(() => {
                System.Threading.Thread.Sleep(3000);
                return DateTime.Now;
            }).ConfigureAwait(false);
  1. You can start the async task on a background thread so that there isn't a synchronization context to capture.
private void Button_Click(object sender, RoutedEventArgs e)
{
    var task = Task.Run(() => App.TestAsync());
    var dateTime = task.Result;
}
  1. you can use async up the whole stack
private async void Button_Click(object sender, RoutedEventArgs e)
{
    Task<DateTime> task = App.TestAsync();
    var dateTime = await task;
}
  1. Given how you are using it, if you don't have to wait until the task is done, you can just let it go and it will finish eventually, but you lose the context to handle any exceptions
private void Button_Click(object sender, RoutedEventArgs e)
{
    //assigning to a variable indicates to the compiler that you
    //know the application will continue on without checking if
    //the task is finished. If you aren't using the variable, you
    //can use the throw away special character _
    _ = App.TestAsync();
}

These options are not in any particular order, and actually, best practice would probably be #3. async void is allowed specifically for cases like this where you want to handle a callback event asynchronously.

CodePudding user response:

From what I understand, in .NET many of the front ends have a single UI thread, and therefore must be written async all the way through. Other threads are reserved and utilized for things like rendering.

For WPF, this is why use of the Dispatcher and how you queue up work items is important, as this is your way to interact with the one thread you have at your disposal. More reading on it here

Ditch the .Result as this will block, rewrite the method as async, and call it from within the Dispatch.Invoke() and it should run as intended

  • Related