So far in this chapter, I've made the assumption that threads don't throw exceptions. Back in the real world, you need to be able to trap and deal with errors in threads launched by your applications.
An exception thrown by any thread that your application launches from its main thread is not propagated back to the main thread. The CLR simply swallows the exception and either returns the thread to the thread pool (if it came from thread pool) or just terminates the thread.
You can trap these thread exceptions by creating an unhandled exception filter and attaching this filter to the Application.ThreadException event. This process is described in detail in Chapter 13. An alternative possibility for threads that run in the thread pool is to add a Try End Try block on the thread start delegate's EndInvoke method. EndInvoke on a thread delegate is the method used to block and wait for the thread to finish.
In the case of the ThreadGui application, the calculation thread is launched asynchronously, so it doesn't make sense to use EndInvoke after the thread has been launched. This would just force the user interface thread to block and wait for the calculation thread to finish, which defeats the point of using an asynchronous thread. Instead, you can call the EndInvoke method after the calculation thread has signaled its completion by calling the CalcComplete callback. Listing 14-16 shows you how you can modify the CalcComplete method shown in Listing 14-15 to catch any exception thrown by the calculation thread.
Private Sub CalcComplete(ByVal CalcResult As System.IAsyncResult) Dim Result As AsyncResult = CType(CalcResult, AsyncResult) Dim MyDelegate As CalcDelegate = _ CType(Result.AsyncDelegate, CalcDelegate) 'Called when asynch thread completes Me.cmdCalc.Enabled = True Me.cmdCancel.Enabled = False Try 'Find out if anything dodgy happened in the async thread MyDelegate.EndInvoke(CalcResult) Catch Exc As Exception MsgBox(Exc.Message, MsgBoxStyle.OKOnly, _ "Async thread exception") End Try End Sub
The first two lines in Listing 14-16 are a little confusing. Their job is to extract the original delegate from the asynchronous result returned by the calculation thread. Once the original delegate has been extracted, the line shown in bold calls EndInvoke on the original delegate. This has the effect of marshalling any exception that occurred in the calculation thread back to the user interface thread, where the Catch block shown here traps and displays the exception message.
If you throw a test exception from the CalculateAccumulation method shown in Listing 14-12, you should now see the displayed exception message. To throw a test exception, simply add a line such as
Throw ApplicationException("Test exception")
Explicitly terminating a managed thread should be done with some care. You can use the Thread.Abort method to terminate a thread, but when doing this you should be aware of exactly how the thread is terminated and the issues that this might cause. This section discusses these issues.
Using Thread.Abort doesn't end a thread immediately. It causes an exception of type ThreadAbortException to be generated in the thread to be aborted, which in turn unwinds any Try End Try blocks in that thread's call stack. Code in related Catch and Finally blocks will be executed, and theoretically this code might perform long, or even infinite, calculations. This means that you can't guarantee that a thread will end when you call Thread.Abort .
Unlike a normal exception, an exception of type ThreadAbortException can't be suppressed by using a Catch block because it's always rethrown automatically at the end of each Catch block. However, a thread with sufficient privilege can call Thread.AbortReset to suppress this exception. This is another way in which a thread might resist being terminated.
To confirm that a thread really has terminated, you need to call Thread.Join . This joins your invoking thread to the thread that's been aborted and blocks your thread until either the joined thread has actually been aborted or the time-out that you specify in the Thread.Join has been exceeded.
If the ThreadAbortException caused by the call to Thread.Abort interrupts a thread during execution of a Finally block, that execution of that block of code won't be completed. This is one of the very few ways in which a Finally block can be bypassed.
Developers will often use Try Catch Finally to protect code where they anticipate that an exception might be thrown. The problem is that an exception of type ThreadAbortException can occur at any time and with no warning. This can complicate the process of writing really safe code that always unwinds itself when interrupted .
Aborting a thread with Thread.Abort unlocks any synchronization locks that the thread holds. This means that the data being protected by these locks may become inconsistent or corrupted, as discussed previously in the section on data race problems.
In some circumstances, attempting to abort a managed thread that's suspended by user code (as opposed to one suspended by the garbage collector or another system process) leads to that thread hanging forever and not terminating. This appears to be a known CLR bug at the time of this writing.
A better way of terminating a thread is to set a PleaseStop variable that the thread can check periodically. This allows a thread to terminate itself under controlled conditions. An example of using this technique safely without synchronization problems is discussed in the ThreadGui application earlier in this chapter.
The final fact to be aware of is that background threads are always terminated automatically when the process or thread from which they were launched is terminated. Try End Try blocks are unwound normally when this happens.