Dispatcher 操作的执行顺序是什么Invoke/BeginInvoke

问题描述

这是WPF中的测试代码:

            Action ac0 = delegate
            {
                Console.WriteLine("action-beginInvoke-enter");
                Application.Current.Dispatcher.BeginInvoke(new Action(delegate
                {
                    Console.WriteLine("dispatcher begin invoke code");
                }));
                Console.WriteLine("action-beginInvoke-exit");
            };
            ac0.BeginInvoke(null,null);
            Console.WriteLine("ui thread sleep before");
            Thread.Sleep(1000);// ensure the ac0 is done
            Console.WriteLine("ui thread sleep after");
            Application.Current.Dispatcher.Invoke(new Action(delegate
            {
                Console.WriteLine("dispatcher invoke code");
            }));
            Console.WriteLine("ui thread exit");

输出:
ui线程先休眠
动作开始调用输入
动作开始调用退出
ui线程在
之后睡眠 调度员调用代码
ui线程退出
调度员开始调用代码

然后,我将 Invoke 代码移到另一个项目中并编译为 .dll:

            Action ac0 = delegate
            {
                Console.WriteLine("action-beginInvoke-enter");
                Application.Current.Dispatcher.BeginInvoke(new Action(delegate
                {
                    Console.WriteLine("dispatcher begin invoke code");
                }));
                Console.WriteLine("action-beginInvoke-exit");
            };
            ac0.BeginInvoke(null,null);
            Console.WriteLine("ui thread sleep before");
            Thread.Sleep(1000);// ensure the ac0 is done
            Console.WriteLine("ui thread sleep after");
            Test.WriteLine();// Test is a static class from dll
            Console.WriteLine("ui thread exit");

这是 Test.WriteLine:

        Application.Current.Dispatcher.Invoke(new Action(delegate
        {
            Console.WriteLine("dispatcher invoke code");
        }));

输出为:
ui线程先休眠
动作开始调用输入
动作开始调用退出
ui线程在
之后睡眠 调度员开始调用代码
调度员调用代码
ui线程退出

我正在尝试找出 Dispatcher 操作的执行顺序。据我所知,UI 线程很忙,直到执行到最后一行。在此之前它如何执行代码“调度程序开始调用代码”?代码是相同的,除了 Dispatcher.Invoke 被删除到一个 dll 中。为什么他们的输出不同?

解决方法

对您的问题的简短回答 - 执行顺序是什么 - 是您拥有它的方式,它是不可预测的。但是我们可以通过使用 TaskCompletionSource 使其可预测。

答案很长——这里有几件不同的事情,我认为这是您意外结果的来源。

首先,您在 Action.BeginInvoke 上使用 ac0。这会异步调用 ac0,即在后台,可能(如果不总是?)在与调用 BeginInvoke 不同的线程上。除非您采取特定步骤(我将在下面概述),否则委托与其他代码的执行顺序是未定义的,并且可能看起来是随机的。

其次,在委托 ac0(同样是异步运行的)中,您将使用另一个委托调用 Dispatcher.Invoke。我们称之为ac0_1ac0_1 是实际写入控制台的内容。 Dispatcher.InvokeAction.BeginInvoke 的不同之处在于,您提供给 Dispatcher.Invoke 的任何内容始终在可预测的线程(UI 线程)上执行。 (使用 Action.BeginInvoke 你不知道它将在哪个线程上执行,除了它(当然?)不会是 UI 线程)。 Dispatcher.Invoke 的不同之处还在于它在委托完成之前不会返回 - 这意味着它会阻塞调用线程 - 这使得与异步操作混合使用非常危险。

因此,通过混合 Dispatcher.InvokeAction.BeginInvoke,您将苹果和橙子混合在一起,并且很少会获得可预测的结果。因此,除非您想处理无法管理的回调链和/或整个应用程序锁定的高潜力,否则 IMO 保证代码按您期望的顺序执行的最佳方法是使用 TaskCompletionSource。它会像这样工作:

        TaskCompletionSource<object> task1 = new TaskCompletionSource<object>();        
        Action ac0 = delegate
        {
           Application.Current.Dispatcher.InvokeAsync(new Action(delegate
           {
              Console.WriteLine("ac0");
              task1.SetResult(null);
           }));
        };
        ac0.BeginInvoke(null,null);
        await task1.Task; // ensure the ac0 is done

        TaskCompletionSource<object> task2 = new TaskCompletionSource<object>();
        Application.Current.Dispatcher.InvokeAsync(new Action(delegate
        {
           Console.WriteLine("ac1");
           task2.SetResult(null);
        }));
        await task2.Task; 
        Console.WriteLine("over");

幕后发生的事情是,await task1.Task 之后的所有内容本质上都变成了在调用 task1.SetResult(null) 时执行的回调,但它仍然允许您编写具有明显线性执行流程的代码。

您还会注意到我将 Dispatcher.Invoke 更改为 Dispatcher.InvokeAsync。为什么?避免锁死的可能性。当您第一次调用 Dispatcher.Invoke 时,我们不知道哪个线程实际处于活动状态,但无论它是什么,都会被阻塞,直到委托完成。由于我们实际上从不想阻塞线程(因此实际上从不想使用 Thread.Sleep),因此最好始终使用这些方法的异步版本并使用 TaskCompletionSource 来获得可等待的 Task在继续之前确保项目已完成。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...