Wednesday, October 21, 2009

 

WinForms App Locked Up - Stack shows OnUserPreferenceChanged leading to a WaitOne after unlocking workstation

Today I had an interesting bug to track down in which an application kept locking up after I locked and then unlocked my workstation. I was able to attach the debugger, do a "Break All" and then use the Thread and Call Stack windows get a trace stack. Somewhere down in the .NET framework, the Microsoft.Win32.SystemEvents.OnUserPreferenceChanged method raised the UserPreferenceChanged event, and an event handler attached to that event ultimately lead to WaitOne which was blocking indefinitely on the UI thread. The top of the callstack looked like this:


[In a sleep, wait, or join]
mscorlib.dll!System.Threading.WaitHandle.WaitOne(long timeout, bool exitContext) + 0x2f bytes
mscorlib.dll!System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) + 0x25 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle waitHandle = {System.Threading.ManualResetEvent}) + 0x77 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control caller, System.Delegate method, object[] args, bool synchronous) + 0x393 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.Invoke(System.Delegate method, object[] args) + 0x50 bytes
System.Windows.Forms.dll!System.Windows.Forms.WindowsFormsSynchronizationContext.Send(System.Threading.SendOrPostCallback d, object state) + 0x5f bytes
System.dll!Microsoft.Win32.SystemEvents.SystemEventInvokeInfo.Invoke(bool checkFinalization = true, object[] args = {object[2]}) + 0x62 bytes
System.dll!Microsoft.Win32.SystemEvents.RaiseEvent(bool checkFinalization = true, object key = {object}, object[] args = {object[2]}) + 0x10f bytes
System.dll!Microsoft.Win32.SystemEvents.OnUserPreferenceChanged(int msg, System.IntPtr wParam, System.IntPtr lParam) + 0x77 bytes
System.dll!Microsoft.Win32.SystemEvents.WindowProc(System.IntPtr hWnd = 9111196, int msg = 8218, System.IntPtr wParam = 47, System.IntPtr lParam = 188618224) + 0x2e0 bytes
 

Presumably, whatever was suppose to signal the WaitOne was also on the main UI thread, so there was a deadlock. Whatever was wrong was apparently happening deep-down in the framework, as there was nothing in the application source code that ever attached an event handler to the UserPreferenceChanged event.

After Googling for some answers, it became apparent that there was probably a threading problem somewhere in the application (as if you couldn't have assumed that one?). Apparently, all top-level controls automatically register for the UserPreferenceChanged event so that they can update themselves when the WM_SETTINGCHANGE Windows message is received to indicate that desktop properties (color, theme, etc.) have changed. And, when SystemEvents raises an event to a partiuclar event handler, it checks to see if it is already on the thread appropriate for the object receiving the event; if it is not, it Invokes over to the appropriate thread. But, if the thread onto which the event handler is invoked does not have a message pump, a lockup occurs (summarized from From kbalertz.com) because Invoke waits forever.

Even with that bit of knowledge, it became difficult to track down the problem. But, "Burn me now, or burn me later" -- I eventually deteremined that it was caused by a cross-threading issue that occurred earlier in the application; for some reason, Visual Studio did not detect it as a cross-thread exception. There was a spot in code that used a System.Timers.Timer, and the callback for the Timer was not properly marshalling calls over to the UI thread before updating some UI properties. Somehow, that cross-threading issue "in the past" affected the ability for a some control to process the UserPreferenceChanged event in the future.

Wednesday, October 7, 2009

 

InvalidOperationException - "Cross-thread operation not valid..."

Most C# programmers have probably seen the infamous InvalidOperationException "Cross-thread operation not valid" exception at one time or another. When you see that, you instantly know that your code tried to access the property of a control from a thread other than the thread on which the control's window handle was created. If you recall a previous post of mine (or from MSDN), there are actually only a total of four methods and properties on a control that can be access from another thread:



Novice programmers may think these cross-thread exceptions are just nuisance messages that can be ignored because they do not happen in the final release build sent to the customer. That, however, couldn't be further from the truth. Rather, you should aggressively seek them out and fix them because they can lead to all kinds of unexplained lock-ups and crashes that are difficult to diagnose. But, in order to find them you have to know when you can see them!

I use to think that cross-thread exceptions are thrown in debug builds, but not thrown in release builds. That seems to be the way it works, at first, and makes sense to me as a programmer. After all, a simple #if (DEBUG) somewhere down in framework code could easily accomplish that. But, as I have found out the hard way, it is not that simple.

Rather, my experimentation has shown that cross-thread exceptions only occur if you actually started the application from within Visual Studio. If you run a debug build of the application from outside of Visual Studio, you do not get the cross-thread exceptions even if you "attached" Visual Studio to that application via the "Debug | Attach to Process..." menu.

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]