Trying to call Async method synchronously. It wait

2019-07-23 04:42发布

问题:

This question already has an answer here:

  • Synchronously waiting for an async operation, and why does Wait() freeze the program here 4 answers

So I'm writing an application in which I want to expose a series of methods with both synchronous and asynchronous equivalents. To do this, I figured the easiest approach was to write the logic in the asnyc method, and write synchronous methods as wrappers around the async methods, waiting synchronously for them to deliver their results. The code isn't playing ball. In the following code sample (not my real code but a reduction of the basic problem), the line Console.WriteLine(result) is never reached - the preceding line hangs forever. Curiously though, if I copy this pattern more or less verbatim into a Console app, it works.

What am I doing wrong? Is this simply a bad pattern, and if so, what pattern should I be using instead?

public partial class MainWindow : Window {

    public MainWindow() {
        this.InitializeComponent();
        var result = MyMethod(); //Never returns
        Console.WriteLine(result);
    }

    public string MyMethod() {
        return MyMethodAsync().Result; //Hangs here
    }

    public async Task<string> MyMethodAsync() { //Imagine the logic here is more complex
        using (var cl = new HttpClient()) {
            return await cl.GetStringAsync("http://www.google.co.uk/");
        }
    }

}

回答1:

This is a classic deadlock. The UI is waiting on the async method to finish, but the async method trys to update the UI thread and BOOM, deadlock.

Curiously though, if I copy this pattern more or less verbatim into a Console app, it works.

That's because your WinForm application has a custom SynchronizationContext. It is caputred implicitly and its job is to marshal work back onto the UI thread once returning from your await.

Should you really expose synchronous wrappers around asynchronous operations?, the answer is no.

There is a way out of it, but i dont really like it. If you absolutely have to (you don't) call your code synchronously (again, you really shouldn't), use ConfigureAwait(false) inside the async method. This instructs the awaitable not to capture the current synccontext, so it wont marshal work back onto the UI thread:

public async Task<string> MyMethodAsync() 
{ 
    using (var cl = new HttpClient()) 
    {
        return await cl.GetStringAsync("http://www.google.co.uk/")
                       .ConfigureAwait(false);
    }
}

Note that if you do this and then try to call any UI element afterwards, you'll end up with an InvalidOperationException since you won't be on the UI thread.

Initializing the UI via a constructor is a common pattern. Stephan Cleary has a very nice series on async which you can find here.

What am I doing wrong? Is this simply a bad pattern, and if so, what pattern should I be using instead?

Yes, absolutely. If you want to expose both asynchronous and synchronous APIs, use the proper api's which wouldn't get you into this situation (a deadlock) in the firstcase. For example, if you want to expose a synchronous DownloadString, use WebClient instead.



回答2:

This is a common mistake. MyMethodAsync captures the current synchronization context, and tries to resume on the synchronization context (i.e. on the UI thread) after the await. But the UI thread is blocked because MyMethod is synchronously waiting for MyMethodAsync to complete, so you have a deadlock.

You should usually not wait synchronously on the result of an async method. If you really have to, you can change MyMethodAsync so that it doesn't capture the synchronization context, using ConfigureAwait(false):

return await cl.GetStringAsync("http://www.google.co.uk/").ConfigureAwait(false);


回答3:

Others have explained the deadlock situation (which I go into detail on my blog).

I'll address the other part of your question:

Is this simply a bad pattern, and if so, what pattern should I be using instead?

Yes, it is a bad pattern. Instead of exposing both synchronous and asynchronous APIs, let the operation itself determine whether it should be asynchronous or synchronous. E.g., CPU-bound code is generally synchronous, while I/O-bound code is generally asynchronous.

The proper pattern is to actually not expose a synchronous API for an HTTP operation:

public async Task<string> MyMethodAsync() {
    using (var cl = new HttpClient()) {
        return await cl.GetStringAsync("http://www.google.co.uk/");
    }
}

Of course, then the question is how to initialize your UI. And the proper answer there is to synchronously initialize it into a "loading" state, and asynchronously update it to a "loaded" state. That way the UI thread is not blocked:

public partial class MainWindow : Window {
  public MainWindow() {
    this.InitializeComponent();
    var _ = InitializeAsync();
  }

  private static async Task InitializeAsync()
  {
    // TODO: error handling
    var result = await MyMethodAsync();
    Console.WriteLine(result);
  }
}

I have another blog post that addresses "asynchronous initialization" with a few various approaches.