问题描述
我有一个带有按钮的 WPF 程序,它创建并显示一些数据绑定到网格的数据。创建数据的过程非常缓慢且受 cpu 限制,因此我将其卸载到任务中。我想在准备好后立即显示第一个数据块,然后显示第二个数据块。
这里有 3 个实现,它们都可以工作并保持 UI 响应。
await 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 的第三个示例是正确的,但我不完全确定。
解决方法
Dispatcher.Invoke 和 InvokeAsync 都在调度程序的线程上执行委托。前者是同步进行的,会阻塞调用线程,直到委托完成;后者不会阻塞调用线程。
这两种方法都根据 DispatcherPriority 参数将委托排入调度程序处理队列中的某处(除非您使用发送优先级,否则 Dispatcher.Invoke 可能会绕过队列并立即调用委托)。因此,优先级越低,调用线程在等待完成时可能被阻塞的时间越长(如果您使用 Dispatcher.Invoke)。
第三种方法,Task.Run(() => Dispatcher.Invoke()),不会阻塞原来的调用线程,但是会阻塞运行任务的线程(大概是线程池线程) .
Dispatcher.InvokeAsync 是适合您的用例的最佳方法,它专为此目的而设计。
,这不是对所问问题的回答,而是关于 Dispatcher.Invoke
和 Dispatcher.InvokeAsync
之间的区别。我想分享我个人对这两种方法的偏好,即两者都不使用。它们既丑陋又笨重,而且大部分都是多余的。 Task.Run
足以将工作卸载到 ThreadPool
,然后等待创建的 Task<TResult>
足以获取计算结果,并在 UI 线程上使用它:
public async void ClickHandlerCommand()
{
var items = await Task.Run(() => SlowCPUBoundMethod1());
DataBoundList = new ObservableCollection<BigObject>(items1);
RaisePropertyChanged(nameof(DataBoundList));
var items2 = await Task.Run(() => SlowCPUBoundMethod2());
DataBoundList2 = new ObservableCollection<BigObject>(items2);
RaisePropertyChanged(nameof(DataBoundList2));
}
如果 UI 和后台线程之间需要更精细的通信,可以使用一个或多个 IProgress<T>
对象来建立这种通信。后台线程被传递一个 IProgress<T>
对象并使用它以抽象的方式报告进度,UI 线程接收这些进度通知并使用它们来更新 UI。可以在 here 中找到使用 IProgress<T>
接口的示例。这也是一本很好的读物:Async in 4.5: Enabling Progress and Cancellation in Async APIs。