WPF 调度程序执行多个执行路径

发布于 2024-08-31 07:26:09 字数 1944 浏览 4 评论 0原文

好吧,周末我发现了一些奇怪的事情。我有一个 WPF 应用程序,它会生成一些线程来执行后台工作。然后,这些后台线程将工作项发布到我的同步上下文。除了一种情况外,一切都工作正常。当我的线程完成时,有时它们会向调度程序发布一个操作,该操作将打开一个弹出窗口。最终发生的情况是,如果两个线程都在调度程序上发布一个操作,它就会开始处理一个操作,然后如果我使用 Window.ShowDialog() 打开一个弹出窗口;当前执行路径按其应有的方式暂停,等待来自对话框的反馈。但问题出现了,当对话框打开时,调度程序立即开始运行发布的第二个操作。这会导致执行两个代码路径。第一个消息框保持打开状态,而第二个消息框正在疯狂运行,因为我的应用程序状态未知,因为第一个操作从未完成。

我发布了一些示例代码来演示我正在讨论的行为。应该发生的情况是,如果我发布 2 个操作,第一个操作打开一个对话框,则第二个操作应该在第一个操作完成后才运行。

public partial class Window1 : Window {

    private SynchronizationContext syncContext;
    public Window1() {
        InitializeComponent();
        syncContext = SynchronizationContext.Current;
    }

    private void Button_ClickWithout(object sender, RoutedEventArgs e) {
        // Post an action on the thread pool with the syncContext
        ThreadPool.QueueUserWorkItem(BackgroundCallback, syncContext);
    }

    private void BackgroundCallback(object data) {
        var currentContext = data as SynchronizationContext;

        System.Console.WriteLine("{1}: Thread {0} started", Thread.CurrentThread.ManagedThreadId, currentContext);

        // Simulate work being done
        Thread.Sleep(3000);

        currentContext.Post(UICallback, currentContext);

        System.Console.WriteLine("{1}: Thread {0} finished", Thread.CurrentThread.ManagedThreadId, currentContext);
    }

    private void UICallback(object data) {
        System.Console.WriteLine("{1}: UI Callback started on thread {0}", Thread.CurrentThread.ManagedThreadId, data);

        var popup = new Popup();

        var result = popup.ShowDialog();

        System.Console.WriteLine("{1}: UI Callback finished on thread {0}", Thread.CurrentThread.ManagedThreadId, data);
    }
}

XAML 只是一个带有调用 Button_ClickWithout OnClick 的按钮的窗口。如果您按下按钮两次并等待 3 秒钟,您将看到 2 个对话框一个接一个地弹出,其中预期的行为是第一个对话框弹出,然后一旦关闭,第二个对话框就会弹出。

所以我的问题是:这是一个错误吗?或者如何缓解这种情况,以便当第一个操作使用 Window.ShowDialog() 停止执行时一次只能处理一个操作?

谢谢, 劳尔

Ok, so I found something weird over the weekend. I have a WPF app that spawns off some threads to perform background work. Those background threads then Post work items to my Synchronization Context. This is all working fine except for one case. When my threads finish sometimes they will post an action onto the dispatcher that will open up a Popup window. What ends up happening is that if 2 threads both post an action on the Dispatcher it starts processing one, then if I open up a Popup window with Window.ShowDialog(); the current execution path pauses waiting for feedback from the dialog box as it should. But the problem arises that when the dialog box opens the Dispatcher then goes and starts immediately starts running the second action that was posted. This results in two code paths being executed. The first one with a message box being held open, while the second one is running wild because my application state is unknown because the first action never completed.

I've posted some example code to demonstrate the behavior I'm talking about. What should happen is that if I post 2 actions and the 1st one opens up a dialog box the second action shouldn't run until after the 1st action has been completed.

public partial class Window1 : Window {

    private SynchronizationContext syncContext;
    public Window1() {
        InitializeComponent();
        syncContext = SynchronizationContext.Current;
    }

    private void Button_ClickWithout(object sender, RoutedEventArgs e) {
        // Post an action on the thread pool with the syncContext
        ThreadPool.QueueUserWorkItem(BackgroundCallback, syncContext);
    }

    private void BackgroundCallback(object data) {
        var currentContext = data as SynchronizationContext;

        System.Console.WriteLine("{1}: Thread {0} started", Thread.CurrentThread.ManagedThreadId, currentContext);

        // Simulate work being done
        Thread.Sleep(3000);

        currentContext.Post(UICallback, currentContext);

        System.Console.WriteLine("{1}: Thread {0} finished", Thread.CurrentThread.ManagedThreadId, currentContext);
    }

    private void UICallback(object data) {
        System.Console.WriteLine("{1}: UI Callback started on thread {0}", Thread.CurrentThread.ManagedThreadId, data);

        var popup = new Popup();

        var result = popup.ShowDialog();

        System.Console.WriteLine("{1}: UI Callback finished on thread {0}", Thread.CurrentThread.ManagedThreadId, data);
    }
}

The XAML is just a Window with a button that calls Button_ClickWithout OnClick. If you push the button twice and wait 3 seconds, you will see you get 2 dialogs popping up one over the other, where the expected behavior would be the first one pops up, then once it's closed the second one will pop up.

So my question is: Is this a bug ? or how do I mitigate this so I can have only one action be processed at a time when the first action halts execution with a Window.ShowDialog() ?

Thanks,
Raul

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

吻泪 2024-09-07 07:26:09

当我正在等待我的问题的答案时(有关使用调度程序的建议优先级和绑定)我认为这会向前支付™。

您正在经历的是调度程序上的嵌套泵送。我建议阅读有关 WPF 线程模型 的 MSDN 文章,尤其是标题为“技术细节和绊脚石”的部分位于页面下方的三分之二处。为了方便起见,描述嵌套泵送的小节复制如下。

嵌套泵送

有时完全锁定 UI 线程是不可行的。
让我们考虑一下 MessageBox 类的 Show 方法。显示没有
返回,直到用户单击“确定”按钮。然而,它确实创造了一个
必须有消息循环才能交互的窗口。尽管
我们正在等待用户点击确定,原来的应用程序
窗口不响应用户输入。然而,它确实继续
处理绘制消息。当以下情况时,原始窗口会自行重绘
覆盖并显露出来。

在此处输入图像描述

某个线程必须负责消息框窗口。 WPF 可以
专门为消息框窗口创建一个新线程,但是这个线程
将无法在原始窗口中绘制禁用的元素
(记住前面关于互斥的讨论)。相反,WPF
使用嵌套消息处理系统。调度程序类包括
一个称为 PushFrame 的特殊方法,它存储应用程序的
然后当前执行点开始一个新的消息循环。当
嵌套消息循环结束,执行在原始消息循环之后恢复
PushFrame 调用。

在这种情况下,PushFrame 在调用时维护程序上下文
MessageBox.Show,它启动一个新的消息循环来重新绘制
背景窗口并处理消息框窗口的输入。当
用户单击“确定”并清除弹出窗口,嵌套循环退出并
调用 Show 后控制恢复。

As I'm awaiting an answer on my question (Advice on using the Dispatcher Priority and Binding) I thought that would pay-it-forward™.

What you are experiencing is Nested Pumping on the Dispatcher. I recommend reading the MSDN article on the WPF Threading Model, especially the section titled 'Technical Details and Stumbling Points' that is two-thirds down the page. The sub-section describing the Nested Pumping is copied below for convenience.

Nested Pumping

Sometimes it is not feasible to completely lock up the UI thread.
Let’s consider the Show method of the MessageBox class. Show doesn’t
return until the user clicks the OK button. It does, however, create a
window that must have a message loop in order to be interactive. While
we are waiting for the user to click OK, the original application
window does not respond to user input. It does, however, continue to
process paint messages. The original window redraws itself when
covered and revealed.

enter image description here

Some thread must be in charge of the message box window. WPF could
create a new thread just for the message box window, but this thread
would be unable to paint the disabled elements in the original window
(remember the earlier discussion of mutual exclusion). Instead, WPF
uses a nested message processing system. The Dispatcher class includes
a special method called PushFrame, which stores an application’s
current execution point then begins a new message loop. When the
nested message loop finishes, execution resumes after the original
PushFrame call.

In this case, PushFrame maintains the program context at the call to
MessageBox.Show, and it starts a new message loop to repaint the
background window and handle input to the message box window. When the
user clicks OK and clears the pop-up window, the nested loop exits and
control resumes after the call to Show.

客…行舟 2024-09-07 07:26:09

模式对话框不会阻止所有者窗口处理消息,否则当模式对话框在其表面上移动时,您会看到它无法重绘(仅作为示例)。

为了实现你想要的,你必须在 UI 线程上实现你自己的队列,可能需要一些同步来在第一个工作项到达时“唤醒它”。

编辑:

此外,如果您在第二个模式对话框启动时检查 UI 线程的调用堆栈,您可能会发现它在堆栈中的上方有第一个 ShowDialog 调用。

编辑#2:

可能有一种更简单的方法可以做到这一点,而无需实现您自己的队列。如果您使用 Dispatcher 对象而不是 SynchronizationContext,您将能够以 优先级调用 BeginInvoke 。 DispatcherPriority.Normal,它将正确排队(检查)。

A modal dialog box does not prevent the owner window from processing messages, otherwise you'd see it fail to redraw as the modal dialog was moved over its surface (just as an example).

In order to achieve what you want, you have to implement your own queue on the UI thread, possibly with some synchronization to "wake it up" when the first work item arrives.

EDIT:

Also if you examine the call stack of the UI thread while the 2nd modal dialog box is up, you might find out that it has the first ShowDialog call above it in the stack.

EDIT #2:

There might be an easier way of doing this, without implementing your own queue. If instead of the SynchronizationContext, you would use the Dispatcher object, you would be able to call BeginInvoke on it with a priority of DispatcherPriority.Normal, and it will get queued properly (check).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文