试图同步调用异步方法。它永远等待Task.Result

时间:2014-11-15 17:47:36

标签: c# .net asynchronous task-parallel-library async-await

所以我正在编写一个应用程序,我希望在其中公开一系列同步和异步等效的方法。为此,我认为最简单的方法是在asnyc方法中编写逻辑,并将同步方法编写为异步方法的包装器,同步等待它们传递结果。代码不是在打球。在下面的代码示例中(不是我的真实代码,但是减少了基本问题),永远不会到达行Console.WriteLine(result) - 前一行永远挂起。但奇怪的是,如果我将这个模式或多或少地逐字复制到一个控制台应用程序中,它就可以工作。

我做错了什么?这只是一个糟糕的模式,如果是这样,我应该使用什么模式?

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/");
        }
    }

}

3 个答案:

答案 0 :(得分:4)

这是一个典型的僵局。 UI正在等待异步方法完成,但异步方法试图更新UI线程和 BOOM,死锁。

  

奇怪的是,如果我将这个模式或多或少地逐字复制到一个   控制台应用程序,它的工作原理。

这是因为您的WinForm应用程序具有自定义SynchronizationContext。它是隐含的,它的工作是从你的await返回后将工作重新编写回UI线程。

Should you really expose synchronous wrappers around asynchronous operations?,答案是

有一条出路,但我真的不喜欢它。如果您绝对必须(您没有)同步调用您的代码(再次,您真的不应该),请在异步方法中使用ConfigureAwait(false)。这指示awaitable不捕获当前的synccontext,因此它不会将工作重新编写回UI线程:

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

请注意,如果您执行此操作然后尝试调用任何UI元素,则最终会得到InvalidOperationException,因为您不会在UI线程上。

通过构造函数初始化UI是一种常见模式。 Stephan Cleary有一个关于异步的非常好的系列,你可以找到here

  

我做错了什么?这只是一个糟糕的模式,如果是这样,那是什么   我应该使用模式吗?

是的,绝对的。如果要公开异步API和同步API,请使用正确的api,它不会让你在第一种情况下陷入这种情况(死锁)。例如,如果要公开同步DownloadString,请改用WebClient

答案 1 :(得分:3)

这是一个常见的错误。 MyMethodAsync捕获当前同步上下文,并尝试在await之后恢复同步上下文(即在UI线程上)。但是UI线程被阻止,因为MyMethod正在等待MyMethodAsync完成,所以你有一个死锁。

您通常不应该同步等待异步方法的结果。如果您真的需要,可以使用MyMethodAsync更改ConfigureAwait(false),以便它不会捕获同步上下文:

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

答案 2 :(得分:2)

其他人已经解释了死锁情况(which I go into detail on my blog)。

我将解决你问题的另一部分:

  

这只是一个糟糕的模式,如果是这样,我应该使用什么模式?

是的,it is a bad pattern。不要暴露同步和异步API,而是让操作本身确定它应该是异步还是同步。例如,CPU绑定代码通常是同步的,而I / O绑定代码通常是异步的。

正确的模式是实际公开HTTP操作的同步API:

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

当然,问题是如何初始化你的用户界面。正确的答案是同步将其初始化为“加载”状态,异步将其更新为“已加载”状态。这样就不会阻止UI线程:

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);
  }
}

another blog post that addresses "asynchronous initialization"有几种不同的方法。

相关问题