How will you make it if you never even try?

April 12, 2005

Control.Invoke() and exception propogation (long version)

Note: This is the “long version” of something I found about Control.Invoke() and exception propogation. Click here for the “short version”.
Just found out something interesting while working on my Home Depot project regarding how exceptions are propogated when using Control.Invoke(). Thought I’d share it.
If you don’t know what Control.Invoke is…
here’s a very brief intro. When you create Windows forms apps, you are using .NET controls such as Button and TextBox. All .NET controls are written in such a manner that they are only safe to access from one thread — the thread which created them. If you’re doing something from a different thread and you want to make something happen to a control (such as change its appearance), then you need to use Control.Invoke(). It takes a delegate, and causes the thread which created the control to call your method. Look here for a more comprehensive discussion, or check out Chris Sells’ book (among others).
We were seeing very WIERD symptoms…
We have a Windows form app with one main UI thread (henceforth called the “main thread”) and one worker thread which communicates with a scanner device attached to an RS232 port (henceforth called the “Scanner thread”). When a scan is received on the scanner thread, it needs to call a method on a control, so the current screen can respond to the “scan event”. However, we need that method to execute on the main thread, so that the scanner can go back to looking for and processing the next scan. Therefore, we use Control.Invoke to marshall the call of the notification method over to the main thread.
In the course of diagnosing some bugs reported by QA, I noticed that the code I was stepping through would simply stop executing! In other words, I would be stepping through in the debugger, say on line 5 of a method with 18 lines in it, and suddenly, it would cease executing that code! It would never execute lines 6-18. Yet, the application would appear responsive — the user can click to open drop downs, resize windows, and click buttons!
Here’s what we found…
After doubting my sanity for a while, I decided to put a try / catch around the last statement that had been executed. The only thing I could think of that would cause execution flow to simply cease was an exception. But, that really didn’t make sense, because we have carefully designed exception handling scheme which includes a top level exception handler that was simply never being reached. Nevertheless, I did place a try/catch around the last statement (statement 5 out of 18 in the method I mentioned above), and sure enough, an exception was occurring.
The exception would have been easy to fix, but the bigger problem now was whatever was causing the exception to be mysteriously swallowed. So, I traced it as it propogated back UP the call stack. Here’s where it got interesting.
What Control.Invoke does with Exceptions
I found that the exception propogated all the way up to our Control.Invoke call. That makes sense. Then, I found that the exception continued to propogate right into the scanner code (an infinite loop that runs on the Scanner Thread). Not only that, but the exception itself was actually marshalled back to the scanner thread. The scanner thread’s loop is based on some code some consultant (in the distant past) found somewhere (perhaps from Microsoft) and plugged in, and the loop is not designed to deal cleanly with any exceptions other than communication errors. As a result, the exception was being swallowed in a way that also caused the scanner to cease to function (the scanner loop was still executing, but it was in a state where it would no longer detect and handle scans).
This was surprising, because we had expected the exception to make its way to the Main Thread’s top level exception handler, just like any other exception thrown on the main thread. Each thread has its own call stack, and exceptions in .NET simply propogate back up the call stack for the thread they occurred on. In this case, therefore, we expected the exception to propogate up to the message pump loop in the main thread, where it would reach our top level exception handler. We never thought it would “cross over” to the scanner thread, and continue up the scanner thread’s call stack.
Using Reflector to Decompile Control.Invoke()
So, I fired up Reflector to see what was going on. Turns out, when you use Control.Invoke(), the .NET framework takes explicit steps to copy the call stack from the originating thread (the scanner thread in this case) over to the destination thread (the main thread in this case). It does this using a framework class called CompressedStack. In .NET 1.1, this class is undocumented (the help system says “This type supports the .NET Framework infrastructure and is not intended to be used directly from your code.”). But interestingly, if you look at the WinFx documentation (aka, Longhorn stuff), you can see documentation for it. Then, since Control.Invoke() is a synchronous call, the originating thread simply blocks (by calling WaitOne()) until the operation has been completed by the destination thread. Over on the destination side, the destination thread performs the operation, catches any exception and stores it in a field, and sets the event that the originating thread is blocking on. Finally, the originating thread (which is now no longer blocked) looks in the field to see if an exception was thrown, and if so, rethrows it on the originating thread. Because of the earlier work that was done to copy the call stack from the originating (scanner) thread, the call stack associated with that exception includes the entire set of calls from the point where the exception had originally been thrown (in the destination thread) to the entry point of the originating thread. So the call stack actually spans calls that were made in 2 separate threads.
Conclusions
So, this explains what I was seeing. When you use Control.Invoke(), any exceptions thrown on the destination thread will indeed propogate all the way back to the Control.Invoke call, at which point the exception will be “marshalled back” to the originating thread so it can continue right up the originating thread’s call stack. This may affect design decisions you make, especially regarding exception handling.
Differences between Control.Invoke() and Control.BeginInvoke() regarding exception handling
Control.BeginInvoke() is implemented by pretty much the same .NET framework code as Control.Invoke(). The only difference is that a flag called isSynchronous will be set to false if you called BeginInvoke. In this case, the framework does not copy the call stack to the destination thread. It also does not block the originating thread by calling WaitOne. Finally, if the destination thread does throw an exception, the framework calls Application.OnThreadException() with that exception — while still on the destination thread — , which would make this exception behave like any other exception on the destination thread … the exception will propogate up to the message pump (unless of course you handled it earlier), and will never be marshalled back to the originating thread. In our case, BeginInvoke turned out to be a better fit than Control.Invoke.
Note that Control.BeginInvoke() is a completely different animal than calling BeginInvoke() on a delegate. Calling BeginInvoke() on a delegate causes your job to be executed on a thread from the .NET thread pool. Therefore, it introduces an additional thread to the picture. But Control.BeginInvoke does NOT introduce an additional thread … it simply marshalls a call over to the control’s thread without waiting for the results and without allowing the exception to propogate back to the originating thread.

Advertisements

Blog at WordPress.com.

%d bloggers like this: