Avoiding a deadlock when creating a STA thread and using Dispatcher.

I have always been pretty articulate and careful when writing multi-threading code, but today I wrote my first deadlock that had me baffled for a few minutes. I'm working on a licensing dll that handles a lot of processing, and it also handles some user interface display using WPF Windows. I cannot guaruntee whether or not the calling thread will be a single-threaded apartment, because the assembly that implements it might be a console or a window, but the assembly doesn't know which, or which threading model it uses. The good news is that System.Threading provides very rich types to be able to check for this as well as handle it.

First lets take a look at actually checking for the threading model, and creating an STA thread if needed.

if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) {
    Thread thread = new Thread(() => {
        // ...
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
}

You can see that the code to check for the current threading apartment isn't anything hard, and neither is setting the apartment state. Now my requirement was that the WPF Window be shown modal by calling Window.ShowDialog so I could consume the DialogResult and continue processing in the licensing service. To do this I created the Window in the newly created STA thread.

if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) {
    ActivateWindowController controller = new ActivateWindowController();
    ActivateWindow window = null;

    bool? dialogResult = null;

    Thread thread = new Thread(() => {
        window = new ActivateWindow(controller);
        dialogResult = window.ShowDialog();
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();
}

There's nothing too exciting about any of this either. I'm using the Model-View-Controller (MVC) pattern so I have a ActivateWindowController that gets passed to the ActivateWindow constructor. Because we need the Window to run in a single-threaded apartment, I do the actual instantiation in the new Thread. Then I make a call to ShowDialog() and consume the result. The last bit is adding thread.Join(); so the calling thread is blocked until the new thread terminates, which is when the dialog is closed.

Now I'll try to explain what ActivateWindowController does as best as I can. When ActivateWindow is loaded, the WPF stack invokes the Loaded event handler. The code isn't anything special, so I'll just show it.

public partial class ActivateWindow : Window
{
    /// <summary>
    /// Initializes a new instance of the DCOMProductions.Licensing.Windows.LicenseWindow class.
    /// </summary>
    public ActivateWindow(ActivateWindowController controller) {
        InitializeComponent();
        _controller = controller;
    }

    #region Controller Members

    private ActivateWindowController _controller;

    private void Window_Loaded(object sender, RoutedEventArgs e) {
        _controller.ActivateComplete += ActivateComplete;
        _controller.Activate();
    }

    private void ActivateComplete(object sender, ActivateWindowController.ActivateEventArgs e) {
        DialogResult = e.Result == ActivationResult.Passed ? true : false;
    }

    #endregion
}

When the Loaded event handler is invoked, a call to ActivateWindowController.Activate() is made. What this method does is spin off a new Task by calling Task.Factory.StartNew(...) and consumes a WCF service. In short, it means another thread. When the task completes, it invokes Task.ContinueWith(...) which is responsible for wrapping up the result and delegating everything to the proper thread.

.ContinueWith((task) => {
    CloseDialogCallback method = new CloseDialogCallback(() => {
        OnActivateComplete(new ActivateEventArgs(result));
    });
    _Dispatcher.BeginInvoke(method, null);
});

Now it is important to note here that _Dispatcher is a private instance that was initialized in the constructor of ActivateWindowController. In short, the dispatcher lives on the UI thread of the Window. The dispatcher is required to delegate calls to the correct thread, in this case we want to delegate the callback method to the UI thread. And remember, the UI thread is the STA thread I created earlier.

I highlighted the last line, because this is where the deadlock is introduced. When _Dispatcher.BeginInvoke(method, null); is called, it schedules asynchronously the callback to be executed on the thread that the dispatcher was created on. And remember, the thread that _Dispatcher was created on is the same thread that the ActivateWindowController instance was created on. Let's double check that.

if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) {
    ActivateWindowController controller = new ActivateWindowController();
    ActivateWindow window = null;

    bool? dialogResult = null;

    Thread thread = new Thread(() => {
        window = new ActivateWindow(controller);
        dialogResult = window.ShowDialog();
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();
}

Well, hopefully by now you realize the problem and why there is a deadlock. ActivateWindowController is not being instantiated on our STA thread. So what's happening here is when _Dispatcher.BeginInvoke is called, it is scheduling the callback to execute on the same thread we told to block by calling thread.Join(). Because that thread is blocked and the dialog can't return until the callback is executed by the dispatcher, I have a blocked thread and a scheduled callback that will never run; eg. a deadlock.

The fix is to instantiate ActivateWindowController on the STA thread I created which is where we want the dispatcher to delegate the callback to anyway. This resolves the deadlock problem. It's important to watch out for little quirks like this. It didn't take me long to realize what created the deadlock, but I think on most days it would have taken me much longer to figure it out. The most important thing to remember is that this is not specific to a Dispatcher, and that it applies to multi-threading regardless.

1 Comment

  1. Samira Anne

    Great data. Appreciate it!. You revealed that superbly.

Leave a Comment