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

本文关键字:永远等待 Task Result 异步方法 同步 调用 | 更新日期: 2023-09-27 18:30:34

所以我正在编写一个应用程序,我想在其中公开一系列具有同步和异步等效项的方法。为此,我认为最简单的方法是在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/");
        }
    }
}

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

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

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

这是因为您的 WinForm 应用程序具有自定义SynchronizationContext。它是隐式捕获的,它的工作是在从await返回后将工作封送回 UI 线程。

你真的应该公开异步操作的同步包装器吗?,答案是否定的

有办法摆脱它,但我真的不喜欢它。如果你绝对必须(你不需要)同步调用你的代码(同样,你真的不应该),请在异步方法中使用ConfigureAwait(false)。这会指示awaitable不要捕获当前同步上下文,因此它不会将工作封送到 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有一个非常好的异步系列,你可以在这里找到。

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

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

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

通常不应同步等待异步方法的结果。如果确实有必要,可以使用ConfigureAwait(false)更改MyMethodAsync,使其不会捕获同步上下文:

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

其他人已经解释了死锁情况(我在我的博客上详细介绍)。

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

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

是的,这是一个糟糕的模式。与其同时公开同步和异步 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。正确的答案是将其同步初始化为"加载"状态,并将其异步更新为"加载"状态。这样 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);
  }
}

我有另一篇博客文章,用几种不同的方法解决了"异步初始化"。