4

一个简单的问题。这是 WinForms 应用程序的一部分:

void Form1_Load(object sender, EventArgs e)
{
    var task2 = Task.Factory.StartNew(() => MessageBox.Show("Task!"),
        CancellationToken.None,
        TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext());

    Debug.WriteLine("Before Exit");
    MessageBox.Show("Exit!");
}

有人可以解释为什么我看到“任务!” 首先,然后当我运行它时“退出”?

当我看到“任务!” 消息框,“退出前”已打印在调试输出中。

编辑:更简单,同样的效果:

void Form1_Load(object sender, EventArgs e)
{
    SynchronizationContext.Current.Post((_) => 
        MessageBox.Show("Task!"), null);

    Debug.WriteLine("Before Exit");
    MessageBox.Show("Exit!");
}

编辑:如果我替换MessageBox.Show("Exit!")Form { Text = "Exit!" }.ShowDialog(),我会看到“退出”,然后是“任务”,正如预期的那样。这是为什么?

4

3 回答 3

4

我猜会发生什么是这样的:

  • 您使用当前同步上下文启动任务。这导致一条消息被(异步)发布到当前线程的消息队列,说“当你处理这个消息时,运行这个代码”。
  • 你做一个MessageBox.Show。这将创建一个新窗口,这会导致将消息(同步)发送到新窗口,作为其创建周期的一部分。
  • SendMessage 的一个副作用是它发送消息因此,发布的消息现在与消息队列中的任何其他内容一起被同步处理。

因此,创建新消息框窗口的行为可能是让任务有机会首先执行的原因。如果您改为执行 Debug.WriteLine 和 Thread.Sleep,您可能会看到相反的情况:Sleep 会首先发生(因为您没有发送消息),然后 Task 会运行。

编辑:根据评论,OP 的行为发生在调用时ShowMessage,而不是在执行时new Form。这些都涉及发送窗口创建消息,因此显然SendMessage不会同步处理已经在队列中的所有消息。(我链接的帖子解释说 SendMessages 确实发送了已发送的消息,但没有具体说明它发送了队列中已经存在的消息——我只是假设后者,显然是错误的。)

我认为我走在了正确的轨道上——关于MessageBox.Show调用的某些事情导致在显示对话框之前处理消息,这与发生的情况不同new Form().ShowDialog()——但我对所涉及的具体机制不正确。其他任何人,请随时研究此问题并提出更准确的答案。

于 2014-02-20T12:46:10.733 回答
4

.NET 框架中 Win32 消息泵送的确切细节没有记录。请注意,Win32 API 历来允许重入行为。

当您的代码启动任务时,它使用的任务调度程序将请求任务在当前的SynchronizationContext. 所以它最终调用SynchronizationContext.Current.Post,与您的第二个代码示例非常相似。

通过WindowsFormsSynchronizationContext将 Win32 消息发布到消息队列来工作;消息类型是 .NET 内部定义的,意思是“执行此代码”。(这些细节没有记录)。

当您的代码继续调用MessageBox.Show时,它会运行一个嵌套的消息循环,这就是执行排队操作的原因。如果您删除了Exit!消息框,那么这些操作将在Form1_Load返回到 中的顶级消息循环后执行Application.Run

于 2014-02-20T13:52:51.207 回答
0

好吧,让我们摆出事实。

  • MessageBox.Show创建自己的消息泵。这使用 current ThreadContext,我假设它与您的情况下的 UI 线程相同 - 换句话说,您的应用程序被冻结。Show是模态的,尽管名称可能暗示。
  • MessageBox不是- 它是由它创建的,也是它的消息泵所在的地方。Formuser32.dll
  • 您创建任务的方法最终会导致将任务推送到ThreadPool.QueueUserWorkItem. 队列似乎是按线程维护的(它是线程静态的)。当您询问任务何时实际执行时,事情会变得复杂,因为现在我们正在处理来自 .NET 外部的异步回调。编辑:我错了。事实上,当前的同步上下文是一个派生类,WindowsFormsSynchronizationContext它实际上确实将工作项放入了与 Windows 消息传递相关联的调用队列中。
  • 一切都发生在一个线程上,就这么简单。
  • 该任务在调试输出之后执行。它与任务内部无关MessageBox.Show

如果我await在调试输出 ( await Task.Delay(1000);) 之后添加另一个,会发生一件有趣的事情 - “任务!” 显示,然后是“退出!” 一秒钟后。同时两个消息框?!这是什么巫术造成的?!

很明显,“退出!” 是“任务!”的模态。形式,而不是我们的父母。换句话说,第二个消息框不知何故必须在第一个消息框的“上下文”中运行。

这与我在原始答案中所说的有关。模态框窃取它正在运行的线程,并处理消息泵送。当第二个await执行时,它在我们的“任务!”上运行。表单,而不是(被阻止的)父表单。

如果我们使用Thread.Sleep(1000);而不是 ,则await此行为将丢失。但是,Thread.Sleep确实在“任务!”之前运行。消息框,事实证明,只要我们关闭“任务!” 表格,“退出!” 立即出现,而不是等待一秒钟,而“任务!” 有延迟。

这些表单依赖于 Windows 消息传递。模态表单“窃取”其所有者的句柄,并处理这些消息。只有在消息框关闭后,才会向父级发送 WM(简单的“设置焦点”消息)。

但是,await在我们的场景中,在 UI 线程上工作,但在消息循环之外。因此,当我们在显示第一个对话框等待时,等待之后的代码就像在第一个对话框中运行一样执行 - MessageBox 的所有者在创建底层本机消息框之前确定(它不是 .NET 表单!) ,所以它获取当前活动窗口 - 在我们等待的情况下,这就是“任务!” 形式。谜团已揭开。

剩下的谜团是为什么任务在MessageBox.Show("Exit!");调用和消息框之间的某个地方运行,实际上窃取了消息循环。

这将我们带到了大结局:

我们创建了我们的小任务。但是,它有一个 windows 窗体同步上下文,所以它不做任何事情,只是将任务添加到窗体上的队列中。这是在队列的顶部,所以一旦我们解除对 UI 线程的控制,它就会被执行。

如果我们在显示“退出!”之前等待。对话框,一切都清楚了——“任务!” 首先显示,并且在某些时候(因为它没有通过消息传递队列),“退出!” 显示为它的孩子。

如果我们不等待,MessageBox.Show("Exit!");将进入一个模态消息循环(我们可以通过事件得知Application.EnterThreadModal)。然后,调用 WinAPI (user32.dll)MessageBox方法,该方法立即抽水。这会读取与我们的排队Invoke调用相关的排队 WM——“Task!”的任务。它立即被调用,并有效地阻止了原始 Message.Show 调用,因为它无法处理自己的消息。

总而言之,另一个不让 UI 线程复杂化的好理由。看起来,尤其是不同的MessageBox.Show,因为它的作用远不止眼前所见。

实际上,您将在 UI 线程之外运行您的任务,并且只有需要访问 UI 的延续才会在 UI 线程中。尽管如此,如何MessageBox劫持发生的事情还是非常有趣的——如果你的后台任务卡在 UI 线程上调用某些东西,这可能会适得其反,而这实际上是由消息框接管的;你的异步性出现了:))

于 2014-02-20T14:17:32.730 回答