.NET 2.0 WinForms multithreading and a few long days

Posted May 19th @ 12:37 am by aaron

The .NET 2.0 SynchronizationContext class and its prominant children, WindowsFormsSynchronizationContext and AspNetSynchronizationContext (no docs), are used to encapsulate the marshalling of code execution back into an appropriate thread context. In the case of the SynchronizationContext class, it simply posts a job to the ThreadPool because there really isn’t a specific threading context that requires special handling. The WindowsFormsSynchronizationContext contains a reference to the thread it was created on (the UI thread) and the Application’s MarshalingControl which allow it to do a BeginInvoke onto the control’s creation thread. The AspNetSynchronizationContext enables ASP.NET to marshal the callback onto the HttpApplication.ThreadContext which helps provide support for the high-scaling “asynchronous pages” feature.

This stuff is all very cool - but can also make some bugs seriously hard to find. Here’s one I ran across this past week.

Our soon-to-be-public .NET API follows one of the async models recommended by Microsoft, providing two flavors of every synchronous method:

void Foo();
void FooAsync(AsyncCompletedEventHandler callback, object state);

Under the covers, we make use of the AsyncOperation and AsyncOperationManager classes found in the System.ComponentModel namespace. Under one particular circumstance I found that the async operation would complete successfully, but the callback would never get called. I traced the code to our call to AsyncOperation.PostOperationCompleted, which completed successfully. Somewhere in frameworkland it just never called the specified callback. That’s when I began looking into the AsyncOperation classes and how they interact with the SynchronizationContext (and what the heck the SynchronizationContext even was). It’s actually relatively simple. When you construct an AsyncOperation, it keeps a copy of the current SynchronizationContext (which could be “plain”, or WindowsForms, or AspNet) and uses the saved copy to post the callback’s execution onto. Nothin’ to it, right?

Well, in my case, a background thread was firing an event that caused a lot of these Async calls (don’t ask :) ), and somewhere in the chain of these many Async calls a subtle bug was causing a Control to be created. When a control is created, or a message loop started, the current synchronization context will be replaced with a new WindowsFormsSynchronizationContext for the current thread, unless the WindowsFormsSynchronizationContext.AutoInstall static property is set to false (it’s true by default). This relies on the fundamental windows rule that all controls must be created on the UI thread. Everything continued executing successfully, except that the sync context was now referencing a MarshalingControl on the wrong thread (a thread without a message loop) so on all subsequent “AsyncCompleted” callbacks, BeginInvoke was being called on the MarshalingControl and would fall into oblivion and never be called.

That itself, aside from my long and boring explanation, wouldn’t have been so bad (after a little research), except that I couldn’t find any instance of a control being created! It ended up that in a relatively recent conversion from the .NET 1.1 ContextMenu to .NET 2.0’s ContextMenuStrip throughout our applications I hadn’t taken into account the fact that referencing a ToolStripMenuItem’s DropDownItems property will create a new DropDown control under the covers automatically if it didn’t already exist. Our application’s “mini”-commanding architecture was causing the UI to update itself when new “actions” were added, but failed to take into account that actions were being added from a background thread (whoops–there’s the root bug). So a simple call to “this.DropDownItems.Add(new ToolStripMenuItem(action));” was creating a control if there were no pre-existing items, and resetting the current SynchronizationContext. One question I have is why I never saw a System.InvalidOperationException in debug mode like I would’ve expected for my cross-thread accessing of the DropDownItems property.

<sigh> It’s times like these that I both feel dumb and feel smart - dumb because they were rather simple fundamental mistakes, and smart because I learned a lot investigating it. I was also able to relay some information to the team that they didn’t know either.

Why do I tell you all this? Let me summarize:

  • ToolStripMenuItem.DropDownItems will create a control if one hasn’t been created. (And a healthy reminder: only reference methods, properties, and fields of controls on the UI thread!)
  • An inadvertant change in the current SynchronizationContext (a static thread-specific property) can cause unexpected conditions - which can be hard to track down because they don’t appear to “fail” explicitly. And in my case, the buggy code was in no way related to the point where I first saw the problem.

Also, if you happen to be so lucky as to be someone writing custom applications using our API, I can assure you that we are dogfooding it like crazy (unlike our last API), and that does nothing but bring pure benefit to you, the customer. :)

2 Trackbacks/Pingbacks

  1. Pingback: windows forms Globalization - Aaron Lerch on January 8, 2008
  2. Pingback: Windows Forms Globalization | Aaron Lerch on January 8, 2008

4 Comments

  1. mike
    August 22, 2007 at 23:29

    Yeah, I didn’t think my example would work, but I had to try something (since I only know about .1% of the namespace :)

    Anyway, nice write up. I’m glad I didn’t have to debug that!

  2. aaron
    August 22, 2007 at 23:29

    The DropDownItems property is fairly unique for a base class library control in how it automatically creates a child control if necessary. Most things, line context menus must be explicitly assigned. So referencing TreeView’s ContextMenu would throw an null reference exception if you haven’t already assigned one.

    But your point is accurate in that this bug could pop up a lot where a control creates another control automatically. Assuming, of course, that the original underlying bug exists of a failure to marshal the execution back to the UI thread.

  3. mike
    August 22, 2007 at 23:29

    Did you just take a job in marketing. From the looks of the last line there, I started wondering…

    Anyway, wouldn’t this bug pop up in a lot of other places where a control uses another control? Like would referencing a TreeView’s ContextMenu property create a new ContextMenu control under the covers automatically if it didn’t already exist and then change the SynchronizationContext as you explained? If not, then I can at least say I tried ;)

  4. Occusiccaky
    November 7, 2007 at 02:54

    Hello!
    How are you?

Leave a comment

OpenID Login

Standard Login

Options:

Size

Colors