【发布时间】:2021-09-04 04:56:12
【问题描述】:
我有一个带有按钮的 WPF 程序,它创建和显示一些数据绑定到网格的数据。创建数据的过程非常缓慢且受 CPU 限制,因此我将其卸载到任务中。我想在第一块数据准备好后立即显示,然后再显示第二块。
这里有 3 个实现,它们都可以工作并保持 UI 响应。
等待 Dispatcher.InvokeAsync、Dispatcher.Invoke 和 Dispatcher.Invoke(在 Task.Run 内)。其中哪一个会避免阻塞线程池上本来可以工作的线程,如果有人在程序的其他地方阻塞了 UI 线程,哪一个最不可能导致死锁?
public ObservableCollection<BigObject> DataBoundList {get;set;}
public ObservableCollection<BigObject> DataBoundList2 {get;set;}
//Click handler from WPF UI button
public async void ClickHandlerCommand()
{
List<BigObject> items1 = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
}).ConfigureAwait(false);
Dispatcher.Invoke(() =>
{
//On UI Thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
await Task.Run(() =>
{
//On thread Y from threadpool
items2 = SlowCPUBoundMethod2();
}).ConfigureAwait(false);
//On thread Y from threadpool
Dispatcher.Invoke(() =>
{
//On UI Thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread Y from threadpool
//5x context switches
}
上述实现将调度程序调用置于Task.Run 之外。这可能会导致两个线程被启动。如果程序中的另一个线程阻塞了 UI 线程,那么我认为 Dispatcher.Invoke 调用可能会死锁?
public async void ClickHandlerCommand2()
{
List<BigObject> items = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
Dispatcher.Invoke(() =>
{
//On UI thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
items2 = SlowCPUBoundMethod2();
Dispatcher.Invoke(() =>
{
//On UI thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread X from threadpool
}).ConfigureAwait(false);
//On thread X from threadpool
//5x context switches
}
上面的实现只有一个线程,但是如果程序中的另一个线程阻塞了 UI 线程,那么我认为 Dispatcher.Invoke 调用可能会死锁?
public async void ClickHandlerCommand3()
{
List<BigObject> items1 = null;
List<BigObject> items2 = null;
//On UI Thread
await Task.Run(() =>
{
//On thread X from threadpool
items1 = SlowCPUBoundMethod1();
}).ConfigureAwait(false);
//On thread X from threadpool
await Dispatcher.InvokeAsync(() =>
{
//On UI Thread
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
});
//On thread X from threadpool
items2 = SlowCPUBoundMethod2();
await Dispatcher.InvokeAsync(() =>
{
//On UI Thread
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
});
//On thread X from threadpool
//5x context switches
}
这应该只会导致 1 个任务被启动,我相信如果其他地方的人阻塞了 UI 线程,可以降低死锁的风险。我认为这是最好的实现方式?
有人可以明确地说哪个是正确的实现吗?我相信使用 await Dispatcher.InvokeAsync 的第三个示例是正确的,但我不完全确定。
【问题讨论】:
-
如果当前任务在线程池线程上运行,则
ConfigureAwait无效(与在 UI 线程上运行时不同)。不能保证在等待之后它会在同一个线程上继续。 -
ConfigureAwait(false)背后的意图是什么?此配置适用于库代码,在应用程序代码中使用它会使您的代码不太可靠,并且其意图更加模糊。有一种更好的方法可以将工作卸载到ThreadPool线程,Task.Run方法,并且您已经在使用它。用ConfigureAwait的东西把事情复杂化有什么意义? -
@TheodorZoulias ConfigureAwait 明确了我在做什么以及我期望发生的事情。默认值为 true,这意味着它将始终上下文切换回捕获上下文。如果你知道你不希望这种情况发生,你可以传入 false 并让它保存一个上下文切换,结果代码在 task.Run 启动的同一线程上运行。我会争辩说“应用程序代码使您的代码不那么可靠,并且它的意图更加模糊”完全相反,它告诉您确切的意图是什么。
-
是的,它看起来很诱人,但您可能想阅读这个问题以了解为什么它可能不是一个好主意:Why was “SwitchTo” removed from Async CTP / Release? 但如果它对您的应用程序有意义,您当然可以考虑走这条路。
-
是的,它是一样的,但它不是 100% 可靠的。这取决于
Task.Run任务在await点未完成,这不能保证AFAIK。
标签: c# wpf multithreading async-await dispatcher