【问题标题】:Task.Factory.StartNew and synchronization contextTask.Factory.StartNew 和同步上下文
【发布时间】:2014-02-20 12:28:45
【问题描述】:

一个简单的问题。这是 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!");
}

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

【问题讨论】:

  • 我认为这证明了 Win32 MessageBox API(由 .NET MessageBox.Show 包装)首先处理所有待处理的消息,然后显示其模式窗口。这与Form.ShowDialog 的作品不同。
  • MessageBox.Show() 是 Application.DoEvents() 的另一个名称,你得到了所有神秘的重入行为,只有减去用户搞砸它的方式。将 async 关键字应用于只触发一次的事件,并且您创建了完美的风暴,将重入叠加在重入之上。这一切确实以一种扭曲的方式有意义,并且完全可以重现,只是很难推理。只有当您编写了所有代码时,才能理解递归。查看堆栈跟踪以了解它。
  • @HansPassant,如果我将MessageBox.Show("Exit!") 替换为Form { Text = "Exit!" }.ShowDialog(),我会看到“退出”,然后是“任务”,正如预期的那样。这是为什么呢?
  • 不同类型的递归,看调用栈。
  • @HansPassant,我删除了 asyncTask.Delay,没有任何改变。我没有看到这里发生了任何递归,堆栈框架证实了这一点。

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


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

    我猜会发生什么:

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

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

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

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

    【讨论】:

    • 您确定吗:这会创建一个新窗口,这会导致将消息(同步)发送到新窗口,作为其创建周期的一部分。 我想即MessageBox运行类似于Application.Run的消息循环,即GetMessage/DispatchMessage消息循环。
    • @avo 仅模式对话框使用它们自己的消息泵运行。这样它们就可以从 ui 线程和块执行中显示出来,但仍然接收输入。
    • 我认为你是对的。我已将新的MessageBox.Show("Exit") 更改为新的Form { Text = "Exit!" }.ShowDialog(),它按预期工作,即“退出!”第一的。我觉得MessageBox.Show先抽出所有的消息,然后显示消息框。
    • 我真的希望MessageBox.Show 表现得像Form.ShowDialog,但显然不是。
    • 好吧,好吧,如果ShowMessage 屈服于任务,但new Form 没有,那么我的解释并不完全正确,它更有可能是ShowMessage 中某些东西的副作用的模态消息循环与Form.ShowDialog 的行为有点不同。其他人可以随意进一步研究并发布比我更好的答案!
    【解决方案3】:

    好吧,让我们摆事实吧。

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

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

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

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

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

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

    但是,在我们的场景中,await 在 UI 线程上工作,但在消息循环之外。因此,当我们在显示第一个对话框后等待时,代码 after 执行 await 就好像它在第一个对话框中运行一样 - 在创建底层本机消息框之前确定 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 线程上调用某些东西,这可能会适得其反,而这实际上是由消息框接管的;你的异步性就这样了:))

    【讨论】:

    • 您创建任务的方法最终会将任务推送到ThreadPool.QueueUserWorkItem。这是错误,这里没有其他线程,只有 UI 线程。你忽略了TaskScheduler.FromCurrentSynchronizationContext()。请参阅@StephenCleary 的回答,它实际上解释了发生了什么。
    • @Noseratio 实际上,我只是忽略了同步上下文是WindowsFormsSynchronizationContext。这最终解释了整个谜团,因为这就是消息如何到达 Windows 消息队列的方式。我将删除所有对ThreadPool 的引用,因为这显然是一个错误的转折点,最终是死胡同。作为一个理性主义者,我不够困惑的事实对我来说是一个巨大的失败:) 我知道没有其他线索涉及。我只是没有意识到ThreadPool 与这种情况有任何关系有多糟糕。过失。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多