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
Leave a Comment
You must be logged in to post a comment.
Great data. Appreciate it!. You revealed that superbly.