How will you make it if you never even try?

April 26, 2005

Control.Invoke() and exception propogation (short form)

I found out some interesting things about Control.Invoke() with regards to exception propogation. This entry is the short form summary of the findings (the long form, which also talks about the symptoms that alerted us about our problem, is here).
Overview of what happens during a Control.Invoke() call
1. Checks are made to determine whether you are already on the thread you need to be on. For the sake of this discussion, I am assuming that you are *not* on the desired thread, and that you therefore really did need to call Control.Invoke (IsInvokeRequired = true).
2. .NET instantiates an instance of type Control.ThreadMethodEntry. This object instance (hereafter referred to as “the Entry Variable”). will simply contain the information needed to call your delegate, and any other information that needs to be “passed over” to the destination thread.
3. When constructing the Entry Variable, .net passes the method you want called, the array of arguments for your method, and the call stack of the originating thread.
4. All those constructor arguments essentially get stored into the state of the Entry Variable instance.
5. .NET then uses the Win32 API call “RegisterWindowMessage” from user32.dll to register a message that can be sent to a Windows OS window.
6. .NET then uses the Win32 API call “PostMessage” to post the message previously registered to the Control’s window.
7. The code for Control.Invoke then blocks by calling WaitOne() on a ManualResetEvent that is a field in the Entry Variable.
8. While the originating thread is blocked, the message pump of the Control you’re targeting continues (as always) to process messages. At some point, it receives the message which was posted in step 6. In Control.WndProc, one of the checks applied to each incoming message is whether its message id is the one that has been used specifically for the purpose of Control.Invoke (the id originally returned from the RegisterWindowMessage call was stored in a field, and is now used to compare against the incoming message).
9. If the message id matches, then you’re now on the destination thread, and the “Entry Variable” (which is part of the state of the Control object) contains the information you need to execute the desired call.
10. The system takes the call stack which is stored in a field in the Entry Variable (the call stack of the originating thread), and then sets the destination thread’s call stack to the same thing. This sets the stage for calling your operation, because the call stack will be such that it contains all the calls from the originating thread that led to your Invoke, *and* all the calls that result from preforming your operation. Note that this means that any exception that arises from your operation can now propogate back to code that was originally called on the originating thread. More on this later in the algorithm.
11. The system calls InvokeMarshalledCallbacks(). This simply does a stardard delegate invocation to execute the operation you specified.
12. The delegate invocation is called inside a try/catch block. The handler catches System.Exception (everything pretty much), and simply takes the exception value and stores it in a field in the Entry Variable. The exception is *not* rethrown, which means it is swallowed.
13. The system then restores the destination thread’s original call stack (to the value it had before it was intentionally overwritten in step 10).
14. The system then calls the Complete() method on the Entry Variable. This results in the ManualResetEvent being signalled, which unblocks the originating thread.
15. The code on the destination thread is now complete. It simply resumes its message pump.
16. Since the ManualResetEvent was signalled, the originating thread is now no longer blocked. Control flow resumes immediately after the WaitOne() call.
17. The originating thread looks into the Entry Variable and sees that its “exception1” field (as named by Reflector) is not null. Therefore, it knows an exception was thrown.
18. NOTE: The call stack associated with this exception has been “tampered with” by the intentional steps listed above which overwrote the threads’ call stacks before invoking your operaton. The call stack for the exception is thus a “splice” of 2 separate call stacks. The top half of the call stack will include all calls that happened on the originating thread from the thread’s birth to the point where you called Control.Invoke. The bottom half of the call stack will include everything that happened on the destination thread, starting at the point that your operation was actually invoked and ending at the point the exception was thrown. The exception naturally propogates back up the call stack. It has already traversed the “bottom half” of the call stack, at which point it was caught, placed in a field, and swallowed. Now, it has been discovered in code that is executing on the originating thread.
19. At this point, the originating thread rethrows the exception. Therefore, it continues propogating back up its call stack, moving backwards from the present spot through all of the calls that led to this point, going all the way to the thread’s birth if not dealt with sooner than that.
In a Nutshell
An exception thrown by your operation that you called via Control.Invoke() will propogate as expected up the destination thread, until it reaches your Control.Invoke(). Then, the exception itself is “marshalled back” to the originating thread. On the originating thread, it will continue to propogate back up all the way to the birth of that thread if allowed.
Differences with Control.BeginInvoke()
The same .NET code handles Control.BeginInvoke() calls. However, there is a flag called isSynchronous which will be false if you called BeginInvoke. As a result of the flag being false, .NET will not monkey with the call stacks of your threads, and it will not call WaitOne() in the originating thread. Finally, if your operation throws an exception, .NET does *not* marshall that exception back to the originating thread. Instead, .NET calls Application.OnThreadException() on the destination thread, which will cause your exception to appear as an unhandled thread exception on the destination thread.

Blog at WordPress.com.