代码之家  ›  专栏  ›  技术社区  ›  avo

Task.Factory.StartNew和同步上下文

  •  4
  • avo  · 技术社区  · 10 年前

    一个简单的问题。以下是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 回复  |  直到 10 年前
        1
  •  4
  •   Stephen Cleary    10 年前

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

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

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

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

        2
  •  4
  •   Joe White    10 年前

    我猜会发生什么:

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

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

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

    我想我走对了—— MessageBox.Show 调用导致消息在对话框显示之前得到处理,其方式与 new Form().ShowDialog() --但我对所涉及的具体机械师并不正确。其他任何人,都可以自由地对此进行研究,并得出更准确的答案。

        3
  •  0
  •   Luaan    10 年前

    好吧,让我们把事实说清楚。

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

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

    很明显,“Exit!”是“Task!”表单的模态,而不是我们的父级。换句话说,第二个消息框以某种方式“在第一个消息框的上下文中”运行。

    这与我在最初的回答中谈到的内容有关。模态框 盗窃 它运行的线程,并处理消息泵送。当第二次 等候 执行,它运行在“任务!”窗体上,而不是(被阻止的)父窗体上。

    如果我们使用 Thread.Sleep(1000); 而不是 等候 ,这种行为将丢失。然而 Thread.Sleep 确实在“任务!”消息框之前运行,这一点可以证明,当我们关闭“任务!“窗体时,“退出!”立即出现,而不是等待一秒钟,而“任务。

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

    然而 等候 在我们的场景中,工作在UI线程上,但在消息循环之外。因此,当我们在第一个对话框显示后等待时 之后 wait就像在第一个对话框中运行一样执行-MessageBox的所有者是在创建基础本地消息框(它不是.NET窗体!)之前确定的,因此它获得当前活动窗口-在我们等待的情况下,这是“任务!”窗体。神秘问题已解决。

    仍然存在的谜团是为什么任务在 MessageBox.Show("Exit!"); 调用和消息框实际上窃取了消息循环。

    这让我们进入了大结局:

    我们创造了我们的小任务。然而,它有一个windows窗体同步上下文,所以它不做任何事情,而是简单地将任务添加到窗体上的队列中。这是在队列的顶部,因此一旦我们解除对UI线程的控制,它就会立即执行。

    如果我们在显示“退出!”对话框之前等待,那么一切都是清楚的-首先显示“任务!”,在某个时刻(因为它没有通过消息队列),“退出。

    如果我们不等待 MessageBox.Show(“退出!”); 将进入模态消息循环(我们可以通过 Application.EnterThreadModal 事件)。然后,WinAPI(user32.dll) 对话框 方法被调用,它会立即泵送。这将读取与队列相关的队列WM Invoke 调用-“任务!”的任务。它会立即被调用,并有效地阻止原始Message.Show调用,因为该调用无法处理自己的消息。

    总而言之,另一个不让UI线程上的事情复杂化的好理由。看起来 MessageBox.Show ,因为它的作用远不止于眼睛。

    实际上,您将在UI线程之外运行任务,只有需要访问UI的继续部分才会在UI线程中。尽管如此,如何 对话框 劫持发生的事情-如果你的后台任务无法调用UI线程上的某个东西,这可能会适得其反,而UI线程实际上是由消息框接管的;这就是你的异步性:))