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
它,并采取相应的行动。
我有一个使用 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
可以完成
- 添加到 app-loop 的东西是 告诉任务它已完成的东西,以便
- 繁荣:僵局
正是因为 UI 线程卡在 .Result
上,app-loop 永远不会 实际上将 .Result
标记为已完成。 通过.ConfigureAwait(false)
改进有一些方法,但这样做的主要目的只是为了避免运行在UI线程上出现问题当您希望它们在后台运行时;这种方法不应该用于防止死锁,即使这恰好是巧合 side-effect.
这里的“修复”很简单:不要使用 .Result
(或 .Wait()
)。您需要 await
它,并采取相应的行动。