【发布时间】: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。我第一次点击它实际上是
- 加载 DLL(冻结我的 UI,如果在 UI 线程上完成)
Text = "Starting"await GetALotOfBlogs();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