等待 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.InvokeDispatcher.InvokeAsync 之间的区别。我想分享我个人对这两种方法的偏好,即两者都不使用。它们既丑陋又笨重,而且大部分都是多余的。 Task.Run 足以将工作卸载到 ThreadPool,然后等待创建的 Task&lt;TResult&gt; 足以获取计算结果,并在 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&lt;T&gt; 对象来建立这种通信。后台线程传递一个IProgress&lt;T&gt; 对象并使用它以抽象的方式报告进度,UI 线程接收这些进度通知并使用它们来更新 UI。使用IProgress&lt;T&gt; 接口的示例可以在here 中找到。这也是一本好书:Async in 4.5: Enabling Progress and Cancellation in Async APIs。

【讨论】:

但是你必须在 UI 线程上执行进度更新。在 MS 文档中明确指出,从后台线程更改集合不会进行。您必须使用调度程序调用。如果您不这样做,某些控件 (devexpress) 会引发异常并将整个应用程序关闭。还必须在 UI 线程上更新 Observable 集合,这在 MS 文档中也有说明。如果你的函数是从一个不是 ui 线程的线程调用的,你的函数将如何工作? @rolls Progress&lt;T&gt; 类在实例化时捕获当前的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 三种等待方式(强制等待、隐式等待、显示等待)

2018-09-21显示等待、隐式等待和强制等待的区别

selenium强制等待,隐式等待,显式等待

selenium中的显示等待,隐示等待,强制等待

sql server等待类型

selenium中的三种等待方式(显示等待WebDriverWait()隐式等待implicitly()强制等待sleep())---基于python