Home arrow Blog arrow Get a worker thread to report an exception back to the main or GUI thread that created it.
Saturday, 17 November 2018
Main Menu
Web Links
Blog Links
Musical Links
Nerdery Links
Other Neato Links
Get a worker thread to report an exception back to the main or GUI thread that created it.
User Rating: / 0
.NET Programming
Written by WATYF on Thursday, 20 December 2007 (179491 hits)
Category: .NET Programming

OK... here's the problem. I'm lazy. But aren't all programmers? I mean... if it wasn't for laziness, we wouldn't have jobs. The whole point of programming is to find something that people don't want to have to do, and write code that will do it for them so they can keep being lazy. Am I right? Tongue out

So when I run into an obstacle while programming, I like to find the simplest way to get around it. If the solutions that are already out there on the internet are cumbersome and complicated, then chances are, I'm not gonna use 'em. And that was the case when I ran into this little problem.

The thing is... most of my applications start new threads. Who wants to wait around staring at a frozen Windows form while something gets processed in the background? But at the same time, you can't just kick off a thread and hope that the user doesn't try to do something else while the thread is running. You have to control the work flow... keep the user in line. So you display some kind of "Please Wait..." dialog and use it to show them the progress of what's going on in the background. But what happens if something goes wrong in the background thread? You can handle the exception in that thread, but how will the main/GUI thread (the one that created the new thread) know that an exception occurred?

....I know how.

You see, sometimes it's a really bad idea to just swallow an exception in a new thread, show an error message, and then continue as if nothing happened. Let's say we have a process that allows the user to enter a URL for a particular kind of file and click "Download". A thread is created that downloads the file, unzips it, reads through the file and checks it for consistency, and then after the thread is done, elements on the UI get updated to display information to the user, such as how long the download took and how big the file was and whether it had any records that didn't pass the consistency check, and so on. It might look something like this:

Sub Main


        'Create a new thread to do stuff
Dim t as new thread(AddressOf TestThread)

'Open a "please wait" form modally
myWait = New frmWait

'After the thread completes successfully, I update some UI controls so the user can see the results of
'what the thread did. This would show record count of the file, the duration of the process, etc.
'If the thread fails, I DON'T want to do this part, that's why the exception needs to be thrown back here
'so that instead of doing this stuff, it'll jump straight to the Catch below.

lblRecord.Text = "Records found in file: " & sRec
lblDur.Text = "The process took " & sDuration
MsgBox "The operation completed successfully."

Catch ex as Exception
'This is where it should go if an error happens in the worker thread.
MsgBox "An error happened during the thread. Details: " & ex.Message
End Try
End Sub
Sub TestThread()
myWait.lblWait.Text = "Downloading file..."
'Code to download a zip file

myWait.lblWait.Text = "Extracting contents of file..."
'Code to extract the contents of the zip file

myWait.lblWait.Text = "Checking file contents..."
'Loop through the contents of the extracted text file and do some consistency checks on the data in the file

Catch ex as Exception
'If an error occurs... I catch it
'This is where I would want to RE-throw the error back to the main thread. If this thread fails I DON'T want
'the main thread to continue about its business as if this thread completely successfully
Throw ex 
End Try
End Sub 

So as you can see... it does me no good to catch the error in the Thread if I can't ALSO let the main thread know that an error happened, so it can skip over the "post thread" code and go straight to the Catch. Otherwise, it'll be updating the "how many records" and "how long the process took" controls for a process that never completed.

But what happens if I re-throw the error in the new thread? I get an unhandled exception. Sure, I could create a global "unhandled exception" handler, but that wouldn't do me any good... it would just allow me to let the user know that an unhandled exception occurred, and then the app will close. What we want is a controlled exception. So how do we get it?

Not by using the System.Threading.Thread object... that's for sure. It's just not powerful enough. Unless, of course, we want to litter our app with global variables that we can check and reset based on whether or not a process completely successfully, or unless we want to try marshalling the exception using Invoke, both of which require you to fashion a custom "exception handler" for every thread that you create in your app. That might work if we had one thread to keep track of, but real-world apps and rarely that simple. We need a simple solution that takes care of itself and doesn't require any over-head programming for every new thread we need to create. So instead, we use the new (with .NET 2.0) BackgroundWorker thread. Now, some of you might have already been thinking, "Why don't you use a BackgroundWorker".... well, the reason is, because it's cumbersome. You have to create a DoWork event and a RunWorkerCompleted event and make a new BackgroundWorker for each process in your application.... cumbersome. I want an object that's as SIMPLE as System.Threading.Thread but as POWERFUL as BackgroundWorker thread. And since that object doesn't exist, we simply make our own. Cool

See... I want to just be able to declare a thread object, assign a Sub procedure to it (like I do with System.Threading.Thread) and run "Thread.Start".  And if an error happens in that thread, I want it to report the error back to the thread that created it. So to do that, I inherit BackgroundWorker and make a super-thread object, like so:

'Enhanced BackgroundWorkerThread to use for threaded processes
Public Class BGWThread
    Inherits BackgroundWorker

    'Public var to hold the exception (if any) that is raised by the BGWThread.
    Public [Exception] As Exception

    Public Delegate Sub MethodDel()
    Private RunProc As MethodDel

    Sub New(ByVal StartProc As MethodDel)
        AddHandler Me.RunWorkerCompleted, AddressOf Me.Completed
        AddHandler Me.DoWork, AddressOf RunStart
        RunProc = StartProc
        Me.WorkerReportsProgress = True
        Me.WorkerSupportsCancellation = True
    End Sub

    Public Function Start() As Boolean
            'Run the bgw

            'Show the Wait form. If you don't want to use a Wait form, you can create an argument in Start that
            'determines how to make the user wait
            myWait = new frmWait

           'If the thread finishes and the bgw.Exception wasn't populated with an exception by Completed, then Return
           '   True. Otherwise, throw the exception/cancel message to the Catch (this is so I can always dispose of
           '   the bgw in the Finally of this Sub instead of having to dispose of it in every Sub that creates it).
            If Me.Exception Is Nothing Then Return True Else Throw New Exception(Me.Exception.Message)
        Catch ex As Exception
            If ex.Message = "Thread Canceled." Then Return False Else Throw ex
        End Try
    End Function

    Private Sub RunStart(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs)
        'Run the procedure that was passed to this BGWThread
        'After the proc is done, see if it got canceled. If so, set e.Cancel to true
        If Me.CancellationPending Then
            e.Cancel = True
        End If
    End Sub

    Private Sub Completed(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
        'This Completed event runs after the thread finishes. It catches any errors that happen within the thread
        '   and allows me to check if the thread was canceled (via the flag that's set at the end of RunStart)
        If e.Cancelled Then 'If the user canceled the operation, set bgw.Exception to say, "Thread Canceled."
            Me.Exception = New Exception("Thread Canceled.")
        ElseIf e.Error IsNot Nothing Then 'If the operation produced an exception, set bgw.Exception to the exception
            Me.Exception = New Exception(e.Error.Message, e.Error)
        End If
        'If the Wait form is still open, close it. This releases the code in RunStart. Since this happens here
        '   (after any bgw thread ends) there is no need to Close frmWait anywhere in any of the threads themselves
        If Not myWait Is Nothing Then
        End If
    End Sub
End Class

OK, so what's happening here is, I've made a Class that inherits BackgroundWorker and enhances it. I created fixed Subs that will be used for the DoWork and RunWorkerCompleted events. And I made a new constructor (Sub New) so I can pass the BGW object the procedure that it's going to run (StartProc), and it assigns the "DoWork" event to that procedure. Then I assign the RunWorkerCompleted event to the Sub that "cleans up" after the thread, and I also set some properties (this may not apply to you) like whether the thread can be cancelled and what not. The Class also has a public variable that stores any Exception that occurs during the thread. The Sub that starts the thread is called (oddly enough) "Start". This Sub will call the RunWorkerAsync (which starts the actual backgroundworker) and then wait for it to complete (using whatever method you want) and then check to see the results of the backgroundworker.

When the thread is done, Completed gets fired... if the user canceled the operation (you'd have to work that out using a modal form) then it stores a "Thread Canceled" exception in Exception, otherwise, it stores the actual error in the Exception variable. So back in "Start", it will check if the Exception var is empty, if so, Start returns "True" (i.e. the thread completed successfully), otherwise, it will throw the actual exception to the Cacth. The Catch will see if the Exception is just a "Thread Canceled." exception, and if so, it will return False (i.e. the thread got canceled)... otherwise, if an actual error occurred, it won't return anything... it will RE-throw the exception back to the main/GUI thread that called "Start" in the first place. This may all sound complicated, but actually USING the BGWThread Class is dead simple. Here's what our new pseudo code would look like using the new Class.

'Declared publicly, so the thread procedure can check it to see if it's been cancelled. Not necessary if you aren't using cancelation
Dim t as BGWThread 

Sub Main


        'Create a new BGWThread to do stuff
t = new BGWThread(AddressOf TestThread)
'Start the BGWThread... if it returns False (i.e. they canceled), then just exit the try 
'   If an error were to occur during TestThread, execution here would jump straight to the Catch below
If Not t.start Then Exit Try

lblRecord.Text = "Records found in file: " & sRec
lblDur.Text = "The process took " & sDuration
MsgBox "The operation completed successfully."

Catch ex as Exception
'Now, this is where it goes if an error happens in the worker thread.
MsgBox "An error happened during the thread. Details: " & ex.Message
End Try
End Sub
Sub TestThread()
          'Check if the user cancelled the process, if so, exit
          If bgw.CancellationPending Then Exit Try
          'Update the message to the user
myWait.lblWait.Text = "Downloading file..."
'Code to download a zip file

If bgw.CancellationPending Then Exit Try
myWait.lblWait.Text = "Extracting contents of file..."
'Code to extract the contents of the zip file

If bgw.CancellationPending Then Exit Try
myWait.lblWait.Text = "Checking file contents..."
'Loop through the contents of the extracted text file and do some consistency checks on the data in the file

Catch ex as Exception
'If an error occurs... I catch it and rethrow it. The benefit of this is that I can do any necessary cleanup
'in the Finally statement of this Try, and therefore, there won't be any stray objects laying around from
'whatever was happening before the error occurred
          Throw ex
          'Cleanup any objects that may have been created by the download/unzip/file check
End Try
End Sub

That may or may not be as clear as mud, but hey... it works great and now that I've got the BGWThread built, I don't have to do any crazy stuff like checking global variables or marshalling exceptions using Control.Invoke or anything else. I just declare a new BGWThread, assign a procedure to it, start it, and the workflow handles itself.


P.S. Some of you may notice that the Wait form is being updated from the new thread, which would cause a cross thread exception in .NET 2.0. That's true, except for the fact that I turn off CheckForIllegalCrossThreadCalls whenever I open the Wait form. Yes, it's cheating, and sloppy, but I only use it to quickly (and easily) update the progress. Any other time I do cross-thread calls (i.e. updating a ListView from another thread or whatever), I use a delegate. If you wanted to, you could add a new Sub procedure to the BGWThread Class and assign the "ReportProgress" event to it. That way, you'd have one place where you could update your Wait form from.


< Prev   Next >


You must javascript enabled to use this form

Thanks for writing this, but have you tried this code as you have written it? I can't get it to work. The exception is still unhandled and doesn't get caught by the main thread.

'bgw' isn't defined, so I changed it to 't'. I also commented out all the forms I don't have then used a bogus statement (file not found) to create an error.

What am I missing?

Posted by bill, on 02/13/2010 at 12:47

Page 1 of 1 ( 1 Comments )
©2007 MosCom

TaskRunner Spam-a-lot

It's cool. It's nifty. It does stuff. It's...

  TaskRunner 3.3!!

Click here to get the brand spanking new version of TaskRunner... 3.3!!
Who's Online

Recent Posts
Most Popular
© 2018 Musical Nerdery
Joomla! is Free Software released under the GNU/GPL License.