【问题标题】:WPF UI freezes with build in -Async methodsWPF UI 因内置 -Async 方法而冻结
【发布时间】:2020-11-21 22:30:26
【问题描述】:

在我的 WPF 应用程序中,我经常使用异步等待模式来保持 UI 响应。但是我注意到它有时会冻结。

我研究了一下,现在我了解到异步方法本身仍然在 UI 线程上运行,因此阻塞是很自然的。

然后我不明白为什么内置异步方法也会冻结我的 UI。例如,如果我调用 await HttpClient().GetAsync(); 我的 UI 将冻结。

为了让我的 UI实际上响应,我需要将该调用包装到 Task.Run(),这将使它在后台线程上执行。

我做了一些实际工作的测试。没有Task.Delay()。 我有一个带有加载动画的简单 WPF 窗口,因此我可以清楚地看到 UI 何时冻结。编辑:添加了更多细节以表明我没有做任何疯狂的事情。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MyViewModel();
    }
}

public class MyViewModel : ObservableObject
{
    public AsyncRelayCommand ButtonCommand { get; }
    public MyViewModel()
    {
        ButtonCommand = new AsyncRelayCommand(Load);
    }

    public async Task Load()
    {
        await GetALotOfBlogs(); // blocks the first time i execute it, 2nd time seems fine
        await Task.Run(GetALotOfBlogs); // does not block ever

        await HttpRequestAsync(); // does not seem to actually block
        await Task.Run(HttpRequestAsync); // does not block

        await ReadFile(); // "laggy blocking"
        await Task.Run(ReadFile); // does not block
    }

    public async Task ReadFile()
    {
        for (int i = 0; i < 100; i++)
            await File.ReadAllBytesAsync(@"C:\Yee\SomeFile.rar");
    }
    public async Task HttpRequestAsync()
    {
        HttpResponseMessage response = await new HttpClient().GetAsync("http://slowwly.robertomurray.co.uk/delay/5000/url/http://www.google.co.uk");
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();
    }

    public Task<List<Blog>> GetALotOfBlogs()
    {
        var ctx = new BloggingContext();
        return ctx.Blogs.ToListAsync();
    }
}

XAML 很简单

<StackPanel>
     <Button Command="{Binding ButtonCommand}" Content="Go"/>
     <local:LoadingSpinner />
</StackPanel>

这似乎足够确凿,一点也不无道理。 “裸”等待语句在 UI 线程上执行,阻止它。 Task.Run 语句在后台线程上执行,而不是阻塞 UI 线程。

我必须从中得出的结论是:在 ViewModels 中,将对 Task.Run 中的异步方法的每个调用包装起来

我阅读了很多关于 WPF 开发的信息。我见过大量的样品和商业项目。然而我以前从未遇到过这种情况。至少不是那么清楚。这让我严重怀疑我的结论。

我错过了什么吗?

编辑:

我想我明白发生了什么。特别是 ef 核心场景对我来说很重要。我注意到即使在任务被击中之前程序也冻结了,尽管之前没有代码

当我第一次单击该按钮时,会加载很多 DLL。我在我的输出窗口'WpfApp3.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\System.Linq.Queryable.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

中收到了一堆这样的消息

Net Core 似乎会根据需要动态加载这些 DLL。看起来 net core 会查看方法 before 执行中的内容,然后加载必要的 DLL。因为 dll 加载发生在实际任务方法之前的任何其他代码之前。

public async Task Button()
{
    Text = "Starting";

    await GetALotOfBlogs();

    Text = "Done";
}

文本绑定到 TextBlock。我第一次点击它实际上是

  1. 加载 DLL(冻结我的 UI,如果在 UI 线程上完成)
  2. Text = "Starting"
  3. await GetALotOfBlogs();
  4. Text = "Done"

这很有趣,因为要执行 Text = "Starting" 它显然不需要所有这些 DLL。

这个 DLL 加载过程是阻止我的 UI 的过程。实际的 ToListAsync 任务不会阻塞。第二次单击按钮执行得很好,毕竟没有要加载的 DLL。

【问题讨论】:

  • 你如何测试它?
  • @Andy:您绝对不需要线程在检索数据时不阻塞。用额外的Task.Run 包装异步 I/O 调用是一种反模式,Task.Run 只能用于for CPU intensive tasks。我的猜测是 OP 可能会在测试结束时调用 ....Result,此时有效地阻止了执行。
  • 它是哪个 AsyncRelayCommand?如果您在异步 Click 处理程序而不是命令中调用 await ((MyViewModel)DataContext).Load(); 会怎样?
  • AsyncRelayCommand 是 Microsoft.Toolkit.MVVM 的一个实现
  • 这取决于他在做什么。现在我更加清醒并再次查看该代码,我不喜欢新的 httpclient() 的外观。每次调用都更新一个 httpclient 是个坏主意。它打算被实例化一次。除了制作单例存在潜在问题之外,Ihttpclientfactory 可能是可取的。docs.microsoft.com/en-us/dotnet/architecture/microservices/…

标签: c# wpf async-await


【解决方案1】:

我可以从经验中看出你的结论是错误的。如果您正在等待的方法是真正异步的,那么将它包装在 Task.Run 中是没有意义的。它有点错过了异步/等待的想法。

你怎么知道你正在等待的方法阻塞了 UI?

我建议确定异步方法的同步部分:

void CallAsyncMethodDoNotAwaitInThisScope()
{
    // start timing code

    // invoke an async method, but do not await on this scope
    // this is how you will measure the synchronous part of the async method
    YourAsyncMethodThatCanPerformAwaitInternally(); // no await here

    // stop timing - measured time should be very short
}

另外,我会在“await”语句之后单独对代码进行计时,以确保它不是罪魁祸首,因为它在 UI 线程上运行。

如果您正在调用的方法的同步部分花费的时间太长,那么将其包装在 Task.Run 中是有意义的。但这通常意味着异步方面没有正确实现。

【讨论】:

  • 这就是我使用异步方法构建的原因。尤其看最后一张。它只是包装实体框架核心 ToListAsync() 任务。我会假设所有这些异步方法都正确实现了,但它们仍然阻塞。
  • 问题是你如何断定他们是阻塞用户界面的人。您需要提供一个可重现的示例。
  • 我注释掉了 6 个异步行中的 5 个。按下按钮并查看微调器。由于人们怀疑我的方法,我只是检查了性能分析器,并且 fps 在同一点下降到 0。
  • 这不是一个正确的测试,因为它没有告诉您代码的哪一部分负责阻塞 UI 线程。
  • 我不明白。我最关心的是 ef 核心测试,它的表现最差。什么其他代码可能是阻塞的?那是我的整个程序。它的 3 行代码。
猜你喜欢
  • 1970-01-01
  • 2014-09-25
  • 2015-01-16
  • 1970-01-01
  • 2018-12-22
  • 2015-09-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多