Form-closing race condition (part 1)

Okay, I know last post I said next I would be writing about more network stuff. But I’ve been away from the blog, and in returning to it I had to revisit an issue that came up when I was doing the GUI stuff. The issue is a race condition between a thread that may need to invoke code on the GUI thread, and the GUI thread itself.


Some brief background: in Windows, GUI objects are tied to the thread on which they were created. There are a variety of ways to deal with this, but in the .NET Forms API, it’s addressed using the Control.Invoke or Control.BeginInvoke method. These methods take a delegate and ensure that it’s executed on the same thread that created the Control instance used to call the method. Invoke is synchronous, not returning until the delegate has been executed, while BeginInvoke returns right away, with the delegate being executed at some later time.


This allows some thread other than the GUI thread to execute code that is only permitted to be executed on the GUI thread. Most commonly, this would be used to allow some worker thread to update the user interface, but another use is to implement an easy form of synchronization.


If you’re reading this, you probably already knew all that. But if you don’t, you probably will want to make sure you understand the above, or review the related concepts on MSDN.


So, what’s the race condition I’m talking about? Consider a form that starts a worker thread, where the form can be closed by the user before the worker thread is done. A natural thing for such a form to do when being closed is to cause the worker thread to interrupt its work and quit. Now consider such a situation, but in addition to all the above the worker thread notifies the form upon completion via an invoked delegate.


Invoking a delegate requires a valid window handle, but a form only has a valid window handle after it’s been displayed but before it’s been closed. If the thread is terminating because the form is being closed, that’s simple enough to deal with: just have the form set a flag, checked before invoking the delegate, before telling the thread to terminate, or simply check the Control.IsDisposed property before trying to invoke the delegate and make sure the thread isn’t instructed to terminate until the form is safely disposed.


But, what if the thread was already terminating on its own, at the same instant that the form is being closed by the user? Racers, start your engines! [:)]


When that happens, there’s a possibility that the thread that is in the process of closing the form will dispose it, but do so just after the worker thread has checked the flag (special-purpose or IsDisposed property) and just before the worker thread actually tries to call Invoke.


If you’ve dealt with race conditions before, you already know what’s coming. One way to address this is to synchronize access to the flag, so that it’s assured to not be changed by one thread while it’s being used by another. It would be nice if we could do this with the IsDisposed property, but we don’t have control over the code that modifies that, so to use this approach would require a new flag. We can set it in the OnFormClosed method and check it before invoking the delegate of interest:


bool _fClosed = false;
object _objLock = new object();

protected override void OnFormClosed(FormClosedEventArgs e)
{
    lock (_objLock)
    {
        _fClosed = true;
    }

    base.OnFormClosed(e);
}

void SomeMethod()
{
    lock (_objLock)
    {
        if (!_fClosed)
        {
            BeginInvoke((MethodInvoker) delegate { });
        }
    }
}

One thing you might notice is that the above code uses BeginInvoke instead of Invoke. With this technique, using Invoke would be a major no-no. Why? Because Invoke introduces a circular lock dependency, which could lead to a deadlock condition. That is, any code executing on the GUI thread has essentially locked the GUI thread, taking the lock that any code trying to call Invoke will want. At the same time, we’ve introduced an explicit lock (_objLock).


If the worker thread gets the explicit lock while at the same time that the GUI thread starts to execute the OnFormClosed method, then we’ll wind up with two different threads, each holding the lock that the other thread needs in order to continue.


Using BeginInvoke gets around this problem by simply queuing the delegate for execution, avoiding the need for the worker thread to ever need to get the lock that’s implicit in the behavior of the GUI thread.


But wait! There’s still a problem. It turns out that there’s a way for a form to be closed without the OnFormClosing or OnFormClosed methods ever being called. Personally, this seems like a design flaw to me. But that’s the way .NET works and we have to live with it. This means that we could conceivably wind up in the method trying to call Invoke (or BeginInvoke) with a disposed form that hasn’t ever set the special-purpose _fClosed flag.


How can this be? Well, it turns out that by default, a .NET Forms application is set up such that there’s one main form, and when that form closes, it terminates the message pump and closes all of the other windows owned by that thread. Because of the way it does that, the other forms never get a chance to execute their …Closing/…Closed methods and events.


One way to deal with this, as well as the original race condition issue, is to just give up and let .NET do it. Wrap the call to Invoke with a try/catch block and let it fail if the form has been disposed before the code trying to call Invoke gets a chance to.


This is actually a fine way to deal with the race condition, I think. The fact is, the overhead of the exception isn’t going to matter, and it’s actually about the simplest way to implement a fix.


But, there’s another technique that when used in conjunction with the explicit synchronized flag will ensure that the synchronized flag technique is reliable. It’s kind of interesting in its own right, and so in my next post I’ll describe that approach.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>