一个简单的问题。以下是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()
,我会看到“退出”,然后是“任务”,如预期的那样。那是为什么?
答案 0 :(得分:4)
我猜这是怎么回事:
因此,创建新消息框窗口的行为可能是让任务有机会首先执行的行为。如果你做了一个Debug.WriteLine和一个Thread.Sleep,你可能会看到相反的情况:睡眠会先发生(因为你没有抽取消息),然后任务就会运行。
编辑:根据评论,在调用ShowMessage
时会发生OP的行为,而在执行new Form
时则不行。这些都涉及发送窗口创建消息,因此显然SendMessage
不同步处理队列中已有的所有消息。 (我链接的帖子解释说SendMessages确实调度了发送的消息,但并没有特别说它已经在队列中发送消息 - 我只是假设了后者,显然不正确。)
我认为我走在了正确的轨道上 - 关于MessageBox.Show
调用的一些信息导致在显示对话框之前处理消息,其方式与new Form().ShowDialog()
不同 - 但我对所涉及的具体机制并不正确。其他任何人都可以随意研究这个并提出更准确的答案。
答案 1 :(得分:4)
.NET框架中的Win32消息抽取的确切细节没有记录。请注意,Win32 API历来允许重入行为。
当您的代码启动任务时,它正在使用的任务调度程序将请求在当前SynchronizationContext
上执行任务。因此它最终调用SynchronizationContext.Current.Post
,非常类似于您的第二个代码示例。
WindowsFormsSynchronizationContext
通过将Win32消息发布到消息队列来工作;消息类型是.NET在内部定义的“执行此代码”。 (这些细节没有记录。)
当您的代码继续调用MessageBox.Show
时,它会运行嵌套的消息循环,这就是排队操作正在执行的原因。如果您删除了Exit!
消息框,则会在Form1_Load
返回Application.Run
中的顶级消息循环后执行操作。
答案 2 :(得分:0)
好的,让我们记下事实。
MessageBox.Show
创建自己的消息泵。这使用当前的ThreadContext
,我假设它与您的案例中的UI线程相同 - 换句话说,您的应用程序被冻结。 Show
是模态的,尽管名称可能暗示。MessageBox
不一个Form
- 它由user32.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 - “任务!”的任务。它会立即被调用,并有效地阻止原始的Message.Show调用,因为它无法处理自己的消息。
总而言之,另一个不要在UI线程上复杂化的好理由。看起来,尤其是MessageBox.Show
的变化,因为它远远超过眼睛。
实际上,您将在UI线程之外运行您的任务,并且只有需要访问UI的延续才会在UI线程中。尽管如此,MessageBox
如何劫持发生的事情仍然非常有趣 - 如果您的后台任务卡在UI线程上调用某些内容,这可能会产生相反的效果,这有效地被消息框接管;那是你的异步性:))