Frank Adams
Frank Adams

Reputation: 113

Why does WPF's Dispatcher.Invoke not cause a deadlock when run on the main thread?

Consider the code:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void button_Click(object sender, RoutedEventArgs e)
        {
            //System.Threading.Thread.CurrentThread = button.Dispatcher.Thread
            button.Dispatcher.Invoke(() => button.Content = "1234");
        }
    }

Of course, button_Click is run on the main thread.

My understanding is that button.Dispatcher.Thread is the main thread and the Invoke() will get processed only when the thread is not blocked. However, isn't the main thread blocked in this case? I.e. the main thread is waiting for the Dispatcher.Invoke() call to complete and Dispatcher.Invoke() is waiting for the main thread to free up. So I expect a deadlock here, but it doesn't get deadlocked.

Why?

P.S: I am aware that in this situation I don't need the Dispatcher.Invoke and that I can call the button.Content = "1234" directly. I am trying to understand why the deadlock DOES NOT happen in this case.

Upvotes: 8

Views: 1566

Answers (2)

nevermind
nevermind

Reputation: 2438

The accepted answer perfectly explains the case when priority is Send, as asked by OP. But things become even more interesting if we specify any other priority.

button.Dispatcher.Invoke(() => button.Content = "1234", DispatcherPriority.Input);

The code above does not dead-lock either, even though it cannot call the method directly (it must process messages with higher priorities first).

In this case WPF puts our message in the Dispatcher's queue, and calls the Dispatcher.PushFrame() method. It basically spins up a nested message loop for us. The inner DispatcherFrame processes queued messages (with higher priorities) until it reaches the one that was placed in the queue by Invoke(). After that, the nested frame is stopped, and execution returns from Invoke() to the calling method.

Btw, that's also how modal dialogs work. So, it might be easier to understand by looking at it. The call to ShowDialog method does not return (remains in the call stack) until the dialog is closed. But the application stays responsive because messages are pumped by the inner DispatcherFrame.

Source code: RefereneSource.

Upvotes: 2

Vassalware
Vassalware

Reputation: 40

I believe your misunderstanding may be based around the following thought process:

"Well, Invoke blocks the calling thread until the action is completed. How can it perform the action on the thread if the thread is blocked?"

If we look inside the source, we see that the callback is being called not only on the same thread, but directly* inside the Invoke method. The main thread is not being blocked.

If you look at the dispatcher's Reference Source page, you can see the following comment above an if statement within the Invoke method's implementation, with the callback being called within it:

// Fast-Path: if on the same thread, and invoking at Send priority,
// and the cancellation token is not already canceled, then just
// call the callback directly.
if(!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
{
    /* snipped */

    callback();

    /* snipped */
}

You're calling Dispatcher.Invoke on the main thread, and the method handles that by just calling it instantly.

*Well, not directly, but the entire body of Invoke(Action) is just a call to the method that the above code is in.

Upvotes: 11

Related Questions