等待 Dispatcher.InvokeAsync 与 Dispatcher.Invoke
Posted
技术标签:
【中文标题】等待 Dispatcher.InvokeAsync 与 Dispatcher.Invoke【英文标题】:await Dispatcher.InvokeAsync vs Dispatcher.Invoke 【发布时间】: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。
【参考方案1】:
Dispatcher.Invoke 和 InvokeAsync 在调度程序的线程上执行委托。前者同步执行此操作,并将阻塞调用线程,直到委托完成执行;后者不会阻塞调用线程。
这两种方法都基于 DispatcherPriority 参数将委托加入到调度程序的处理队列中(除非您使用发送优先级,否则 Dispatcher.Invoke 可能会绕过队列并立即调用委托)。因此,优先级越低,调用线程在等待它完成时可能被阻塞的时间越长(如果您使用 Dispatcher.Invoke)。
第三种方法,Task.Run(() => Dispatcher.Invoke()),不会阻塞原来的调用线程,但会阻塞任务正在运行的线程(推测是线程池线程) .
Dispatcher.InvokeAsync 是最适合您的用例的方法,它正是为此目的而设计的。
【讨论】:
【参考方案2】:这不是对所提问题的回答,这是关于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。使用IProgress<T>
接口的示例可以在here 中找到。这也是一本好书:Async in 4.5: Enabling Progress and Cancellation in Async APIs。
【讨论】:
但是你必须在 UI 线程上执行进度更新。在 MS 文档中明确指出,从后台线程更改集合不会进行。您必须使用调度程序调用。如果您不这样做,某些控件 (devexpress) 会引发异常并将整个应用程序关闭。还必须在 UI 线程上更新 Observable 集合,这在 MS 文档中也有说明。如果你的函数是从一个不是 ui 线程的线程调用的,你的函数将如何工作? @rollsProgress<T>
类在实例化时捕获当前的SynchronizationContext
,并确保将在此上下文中调用通知回调。因此,您可以从回调委托内部安全地与 UI 组件进行交互。
是的,我听到你在说什么,但它仍在执行 dispatcher.invoke,它只是从你的代码中抽象出来的。我们的大多数问题都源于必须在 UI 线程上的 RaiseCollectionChanged 事件,而从 IProgress 等中执行这些事件并没有真正意义。我知道在许多情况下 PropertyChanged 可以从后台线程中完成,但在我们的情况下使用我们使用的控件,这样做是不安全的,因此我提出了这个问题。
这里是对我们所拥有的一些场景的一些讨论。例如,我们需要一个 ItemsControl 的 observablecollection,然后我们在其上引发 collectionchanged。另一个是 TreeList,我们在一些昂贵的操作期间更新了大约 4000 个复选框和背景颜色。 supportcenter.devexpress.com/ticket/details/t661389/…
@rolls 要清楚我知道所有 UI 控件,无论是原生的还是第三方的,都应该只从 UI 线程访问,我不建议在任何地方规避这个规则。以上是关于等待 Dispatcher.InvokeAsync 与 Dispatcher.Invoke的主要内容,如果未能解决你的问题,请参考以下文章
Selenium 三种等待方式(强制等待、隐式等待、显示等待)
selenium中的三种等待方式(显示等待WebDriverWait()隐式等待implicitly()强制等待sleep())---基于python