Task.Factory.StartNew和同步上下文

时间:2014-02-20 12:28:46

标签: c# .net asynchronous task-parallel-library synchronizationcontext

一个简单的问题。以下是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(),我会看到“退出”,然后是“任务”,如预期的那样。那是为什么?

3 个答案:

答案 0 :(得分:4)

我猜这是怎么回事:

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

因此,创建新消息框窗口的行为可能是让任务有机会首先执行的行为。如果你做了一个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线程上调用某些内容,这可能会产生相反的效果,这有效地被消息框接管;那是你的异步性:))