WPF调用异步方法导致阻塞的结果

WPF calling Result of async method causing block

我有一个使用 Prism 的 WPF 应用程序,我发现异步方法有一个额外的行为。

我有一个 class 具有像这样的异步方法

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

我需要以同步形式调用它们,因为我需要 ViewModelManager 的构造函数中的结果,我尝试使用 Result,因为这是以同步方式获取结果的方法之一。

public class ViewModelManager{
    private readonly ConfigurationManager _configManager;

    private void LoadConfiguration(){
        var config = _configManager.LoadConfigurationAsync().Result;
    }
}

令我惊讶的是,这会导致应用程序在 Result 调用中被阻塞,我知道 Result 正在阻塞但并非总是如此,该方法的 return 行永远不会被执行。 我尝试使用 Task.Run 调用它,它有效

private void LoadConfiguration(){
    var config = Task.Run(() => _configManager.LoadConfigurationAsync()).Result;
}

我不知道这里发生了什么,为什么调用结果会阻止应用程序以及为什么使用 Task.Run 它有效。这就像调用两个任务,因为该方法已经 returning a Task.

访问 .Result 总是 一个错误(有一个关于“我已经检查它已经完成”的小警告,还有关于 value-tasks 和单一访问;老实说:“总是”比了解注意事项更有用!)。

在最好的情况下,您已经实现了“异步之上的同步”并且无缘无故地占用了一个线程,影响了可伸缩性并且可能影响了 thread-pool。

但是,如果有同步上下文,您可以——正如您所发现的那样——造成死锁。同步上下文充当工作管理器,在 WPF 的情况下:意味着 默认情况下 漏斗回调和 await 通过 UI 线程。所以想象一下:

  • 您的 UI 线程 运行s,在后台启动一些东西,然后到达 .Result 等待 - 尚未返回到主应用程序循环
  • 您的后台事物完成,并尝试将挂起的操作发信号通知为已完成,即触发与该操作 await 关联的延续
  • await 逻辑说“哦,我看到了一个 sync-context - 我需要通过它推动工作”并向主要 app-loop 添加一些东西
    • 添加到 app-loop 的东西是 告诉任务它已完成的东西,以便 .Result 可以完成
  • 繁荣:僵局

正是因为 UI 线程卡在 .Result 上,app-loop 永远不会 实际上将 .Result 标记为已完成通过.ConfigureAwait(false)改进有一些方法,但这样做的主要目的只是为了避免运行在UI线程上出现问题当您希望它们在后台运行时;这种方法不应该用于防止死锁,即使这恰好是巧合 side-effect.

这里的“修复”很简单:不要使用 .Result(或 .Wait())。您需要 await 它,并采取相应的行动。