A concrete example of how Control.Invoke can cause deadlock

I’ve written a number of times in this blog about deadlock; but I don’t know that I’ve ever given a concrete example illustrating how it can actually occur in code.

The unfortunate truth is that it’s easier to stumble upon than you might think. What I will give shortly is an example using some very common tools many .NET developers should be familiar with: the Windows Forms API and a simple WaitHandle.

But first, a refresher on what deadlock is.

A mother and son negotiating over a broken vase

Deadlock happens when two processes are waiting on each other. In the cartoon above, the boy will not reveal what happened (obvious though it may be) until his mother promises not to be mad. His mother, in turn, can’t make any such promise until she knows what happened.

In a Windows Forms application, it is disturbingly easy to fall into the trap of writing code that results in a deadlock. I find that this generally happens with developers who, to borrow one of my mom’s expressions, “know just enough to be dangerous” about multithreading. These are the kinds of developers who know about the lock statement, about threads and mutexes and wait handles, etc., but who aren’t exactly sure how to wield these tools in a totally safe way.

In all honesty, I have to count myself among these developers, since I can’t say I am so good at multithreading that I never make mistakes in this realm. (To be brutally honest, I don’t think anyone is that good… but I’ve talked about that before.)

So, how can deadlock happen so easily in a Windows Forms app? Simple: through a single careless use of the Control.Invoke method.

To understand this, it’s necessary to first understand how Windows Forms actually works. It’s actually pretty straightforward: Windows Forms basically consists of a message pump:

The Windows message pump

A Windows application is little more than a queue of messages being sent to a window. Every action performed by the user—moving the mouse, hitting a key on the keyboard, clicking on a button, etc.—pushes a “message” into a queue, while Windows continuously pops these “messages” from the queue and executes whatever code a programmer has associated with each message.

(In Windows Forms, these messages are exposed in the form of .NET events: MouseMove, Click, etc.)

Now, a crucial point that many rookie developers miss is that this queue is being processed by a single thread, typically referred to as the “UI thread” or “foreground thread” (these are not formal terms, as far as I know). It needs to be this way because, like probably 95% of software components in existence, the controls in the System.Windows.Forms namespace were not designed to be thread-safe when accessed concurrently from multiple threads. And so calls to methods that affect UI controls—like adding to a ListBox, setting the text of a TextBox, etc.—must be made from the UI thread; otherwise, it will be pandemonium.

So whenever one wants to update the UI from a background thread (by the way, in my personal opinion, you should just never do this at all—this almost always indicates overly tight coupling or an otherwise problematic design—but that’s a subject for another post altogether), the most common way to do so is by calling Control.Invoke.

This is what that does:

  1. Pushes a new message onto the message queue
  2. Waits for that message to be processed

Uh oh… there is a scary word there: “Waits.” Any scenario where your program is waiting is one where deadlock may creep in, if you aren’t careful.

And so here at last is that concrete example I promised (you can grab this in full project form from a new repository I’ve created on GitHub for the sole purpose of hosting code I share on this blog):

Imports System.Threading

Public Class DeadlockExampleForm

    Private _unlocked As New ManualResetEvent(False)

    Private Sub BigButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BigButton.Click
        Dim t As New Thread(AddressOf Deadlock)

        Me.Text = "Entering Deadlock"

        ' This line will wait forever.
    End Sub

    Private Sub Deadlock()
        ' Invoke is a blocking call. This thread will not continue past this
        ' line until the Click event handler above runs. Until _unlocked is
        ' signalled below, this will never happen.
        Invoke(New Action(AddressOf NotifyComplete))

        ' This line will never be reached.
    End Sub

    Private Sub NotifyComplete()
        Me.Text = "Escaped Deadlock"
    End Sub

End Class

See how easy that is? That’s seriously all that needs to happen for deadlock to strike a Windows Forms application: you call Invoke from a background thread, and from the UI thread you wait on something that happens after that Invoke call on the background thread. Now the background thread is waiting (because the message pushed by the Invoke call is not yet processed), and the UI thread is waiting (because the background thread hasn’t done whatever the UI thread is waiting for). Disaster.

Full disclosure: yes, I am familiar with this problem because it has bitten me in the past. Don’t let it bite you!


One thought on “A concrete example of how Control.Invoke can cause deadlock

  1. Carra says:

    Interesting post and one that bit us this week.

    Our foreign colleagues who programmed some parts for us fall in the “dangerous kids” category. As a result, we now have tons of threads and even more invokes. As a result they added some “Sleep(100)” which have to be left alone or it doesn’t work. Explaining to them how dangerous using shared resources is between multiple threads and explaining the “lock” statement lead them to start using those… Which of course made them use lock(a) lock(b) all over the place. And as a result we now have deadlocks, some on the “invoke” part.

    Luckily our main project doesn’t contain a single invoke in the gui dlls 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: