等待 Dispatcher.InvokeAsync 与 Dispatcher.Invoke

问题描述

我有一个带有按钮的 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.InvokeDispatcher.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