BackgroundWorker/RunWorkerCompleteEventArgs bug

(Note: this write up is based on build 2.0.50727.26 of the 2.0 CLR, which is the RC build.  It's possible this problem has been fixed for the final build, although I kind of doubt it...)

I've been playing around quite a bit lately with the SynchronizationContext architecture making it's debut in the 2.0 version of the .NET Framework.  On the whole, I've been pleasantly suprised and plan to post some other write ups here on the ins & outs of leveraging, as well implementing, this pattern.  But I've run into what I believe is a bug in the way the BackgroundWorker and RunWorkerCompletedEventArgs classes are implemented.  It's of the really, really obvious kind; so I'm half wondering if I'm missing something...

At any rate, for those that haven't played with it before, the BackgroundWorker class is basically a component that facilitates performing lengthy operations without interfering with the UI updating work of something like a Windows Forms application, or losing track of the various pieces of important ASP.NET-managed 'context' (like impersonation context) as you use various physical threads to implement asynch pages or web service entry points, or interact with other asynchronous services.  In fact, the BackgroundWorker class really just one example of the more generalized synchronization context architecture that's been introduced in the 2.0 framework.  I'll have more to say about that in future posts.

The basic usage model for BackgroundWorker is to call a method named RunWorkerAsync, which requests the BackgroundWorker class to kick off a call back to you (via the DoWork event you handle) from another thread.  The idea is that in your event handler callback, which is called in the context of a thread pool thread, you'd proceed to “do the lengthy operation”.  If you were using this in a Windows Forms application, this means that your UI won't be frozen while you take your time completing the lengthy operation, whatever it might be.

Over the course of doing this work, you have the option of calling the ReportProgress method of BackgroundWorker to indicate how far along you are towards completing the operation (expressed as a percentage).  If called, ReportProgress uses the synchronization context architecture to arrange for a callback in the “right” context.  Once the callback occurs in the “right” context, BackgroundWorker then fires the ProgressChanged event.  In a Windows Forms application, the “right“ context is the context of the thread that was used to call RunWorkerAsync to begin with.  The result is that your ProgressChanged event handler can safely do whatever it wants to do to react to an update from the BackgroundWorker object.  Anyone that's used the 1.x ISynchronizeInvoke architecture is familiar with this concept.  This is just the new & improved, and now more generalized, solution to this problem.

When the call to your DoWork event handler returns, the BackgroundWorker class then fires its RunWorkerCompleted event.  As with the ReportProgress event, the BackgroundWorker class implementation uses the synchronization context architecture to make sure it's calling your event handler back in the “right” context.

So that's the tutorial/overview.  Here's the problem I've run into.  Pretty much every callback-based API ever invented allows the caller to pass in some sort of cookie/chatchka/widget/objref that's associated with the operation being requested.  The component/library being called treats this parameter as opaque.  Then any time the component/library wants to call you back related to this operation, it passes this cookie back to you.  This allows the caller to (typically) pass in something like a reference to an object where they're keeping track of everything related to the operation; making it easy to figure out what's going on when they're subsequently called back.

BackgroundWorker is (almost) no different.  In typical fashion, one of the overloads of RunWorkerAsync takes a vanilla object reference parameter named 'userState'.  Whatever you pass for this parameter is made available to your DoWork event handler via the Argument property of the DoWorkEventArgs object that you're event handler is passed.  Similarly, you can pass an object reference to ReportProgress that's sent to your ProgressChanged event handler via the UserState parameter of the ProgressChangedEventArgs that's passed to you.  Pretty typical stuff.

The problem/oversight I've run into is that the RunWorkerCompleted event handler does not make any kind of user state parameter available to you.  This seems silly & odd to me.  My expectation was that whatever I passed to RunWorkerAsync would be passed not only to my DoWork event handler, but also my RunWorkerCompleted event handler.  But it's not.  This makes it difficult to figure out in your 'work done' handler what exactly it is that's done.

Consider this simplified example.  Imagine I'm using a really, really slow calculator object - so slow that calling Add in the context of a Windows Forms UI thread freezes the form for an intolerable amount of time.  So instead, I setup & use a BackgroundWorker so that I can push the slow calculation off to another thread.  Here's what the form code might look like that initiates the background operation (_bgw is a reference to a BackgroundWorker):

void _addButton_Click( object sender, EventArgs ea ) {
    int a = int.Parse(_arg1.Text);  // _arg1 & _arg2 are TextBox
    int b = int.Parse(_arg2.Text);  // controls.

    CalcArgs args = new CalcArgs(a, b);
    _bgw.RunWorkerAsync(args);      // userState == ref to CalcArgs
}

The above code just converts a couple of strings into integers, packages them up in a simple CalcArgs class (with A & B properties for fetching those 2 values), and then passes the reference to that CalcArgs object to RunWorkerAsync.  Then here's what the DoWork event handler, which is called in the context of the background thread, might look like:

void _bgw_DoWork(object sender, DoWorkEventArgs e) {
    CalcArgs args = (CalcArgs)e.Argument;
    e.Result = _slowCalc.Add(args.A, args.B);
}

This code just grabs the CalcArgs information that was passed in, and uses that to interact with the really slow calculator.  The result of the computation is then stored in the Result property of the DoWorkEventArgs.  That result, whatever it is, will be made available later in the RunWorkerCompleted event handler via the RunWorkerCompletedEventArgs.Result property.  If the DoWork handler crashes, the BackgroundWorker will trap the exception and make it available via the RunWorkerCompletedEventArgs.Error property.  And if I was doing the lengthy operation right here (say by looping several times), the other outcome might be that I notice that the BackgroundWorker.CancellationPending property has been set to true (via a call to BackgroundWorker.CancelAsync.  If that happens, the DoWork event handler is supposed to set e.Cancel to true instead of setting e.Result to something.

One way or the other, when the DoWork event handler returns control to the BackgroundWorker class, the BackgroundWorker class then fires the RunWorkerCompleted event (in the right context) to inform you of the outcome.  Here's what the typical RunWorkerComplete event handler looks like:

void _bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if( e.Cancelled ) {
        // The operation was cancelled.  Trying to look at e.Result
        // will result in an invalid operation exception.
    }
    else if( e.Error != null ) {
        // The operation crashed.  e.Error indicates the nature of
        // the crash (System.Exception or derived).  Trying to look at e.Result
        // will result in an invalid operation exception.
    }
    else {
        // The operation was neither canceled, nor did it crash, so
        // you can look at e.Result to see what the outcome is.
    }

}

Note that e.Cancelled and e.Error indicate whether or not the operation was cancelled or crashed.  And if neither happened, then e.Result holds the result (if any) of the operation.  In this example, e.Result would be a reference to a boxed integer that held the sum of the two numbers that were added together.

Another property of the RunWorkerCompletedEventArgs class is called UserState.  This parameter is supposed to hold whatever you passed to RunWorkerAsync for the userState parameter.  In our case, this would allow us to do something like this inside our RunWorkerCompleted event handler:

...
else {
    // The operation was neither canceled, nor did it crash, so
    // you can look at e.Result to see what the outcome is.
    //
    CalcArgs args = (CalcArgs)e.UserState;
    _resultLabel.Text = string.Format(“{0} + {1} = {2}“, args.A, args.B, e.Result);

}

The issue is that the UserState property of RunWorkerCompletedEventArgs is always null.

It's easy to see why using something like Reflector.  Here's the psuedo code for the code in BackgroundWorker that calls both the DoWork and RunWorkerCompleted event handlers:

void WorkerThreadStart(object userState)
{
    object result = null;
    Exception error = null;
    bool cancelled = false;
   
    try {
        DoWorkEventArgs dwea = new DoWorkEventArgs(userState);
        FireDoWorkEventInClientContext(dwea);

        if (dwea.Cancel) {
            cancelled = true;
        }
        else {
            result = dwea.Result;
        }
    }
    catch (Exception ex) {
        error = ex;
    }

    RunWorkerCompletedEventArgs rwcea =
        new RunWorkerCompletedEventArgs(result, error, cancelled);
    RaiseRunWorkerCompletedEventInClientContext(rwcea);
}

Note that the caller's original userState argument is passed into the constructor of the DoWorkEventArgs object that's passed to the caller's DoWork event handler.  But notice that the userState parameter is not passed into the constructor of the RunWorkerCompletedEventArgs object that's passed to the caller's RunWorkerCompleted event handler.  And just in case you were wondering if it was somehow magically passed under the covers, a peek at the implementation of the RunWorkerCompletedEventArgs constructor will show that's not the case:

public RunWorkerCompletedEventArgs(object result, Exception error, bool cancelled)
    : base(error, cancelled, null)
{
    this.result = result;
}

Notice the null in the 3rd parameter position in the call to the base class constructor.  The base class is AsyncCompletedEventArgs.  It's this base class that actually provides the public UserState parameter that's exposed via RunWorkerCompletedEventArgs.UserState.  It's the 3rd parameter (userState) that's returned by this property.  But since RunWorkerCompletedEventArgs doesn't provide an opportunity for the BackgroundWorker class to pass the user state argument to its constructor, it has nothing to pass along to AsyncCompletedEventArgs, and so instead just passes null.

The result is that you don't have access to the user state argument that's associated with the operation which just completed.  IMO, this is a bug.  It just makes no sense whatsoever.  Given the lengthy history of callback-based API design, there's no reason this should have happened.  Not to mention the fact that its highly inconsistent for the BackgroundWorker class to propogate the userState parameter from RunWorkerAsync to only one of the two event handlers that are subsequently invoked.

As for work arounds, there are a couple that come to mind.  One is to have your DoWork event handler use the Result property of the DoWorkEventArgs to pass your user state argument to yourself since the BackgroundWorker doesn't do it for you.  This would look something like this:

void _bgw_DoWork(object sender, DoWorkEventArgs e) {
    CalcArgs args = (CalcArgs)e.Argument;
    e.Result = _slowCalc.Add(args.A, args.B);
    args.Result = _slowCalc.Add(args.A, args.B);
    e.Result = args;

}

With this approach, instead of storing the result of the operation directly into DoWorkEventArgs.Result, that result is instead stored in the CalcArgs/userState argument we've been passing around to ourselves.  The reference to the CalcArgs/userState object is then stashed in DoWorkEventArgs.Result.  This bit of indirection would allow your work-completed to be rewritten as follows:

...
else {
    // The operation was neither canceled, nor did it crash, so
    // you can look at e.Result to see what the outcome is.
    //
    CalcArgs args = (CalcArgs)e.UserState;
   
CalcArgs args = (CalcArgs)e.Result;
    _resultLabel.Text = string.Format(“{0} + {1} = {2}“, args.A, args.B, e.Result);
}

However, there's a problem with this work around.  The issue is that if the original DoWork event handler caused either DoWorkEventArgs.Error or DoWorkEventArgs.Cancel to be set, then the DoWorkEventArgs.Result property refuses to return your result to you.  Instead, it throws an exception indicating the operation was either cancelled or failed.

So that work around is only an option for you if you ensure that neither of those two conditions can occur in your code.  If you can't make that guarantee, then you have to resort to a different sort of indirection.  Instead of having the BackgroundWorker call your form back directly, you have it call an intermediate object back when events are fired.  That object, not your form, holds the state you need.  IOW, that object's this/me reference becomes the implicit userState state parameter that you need, but that's not passed to you in RunWorkerCompleted.  It's that kind of silly force-you-to-design-around-it bug that always made the ThreadStart delegate required by the System.Threading.Thread class so lacking, resulting in the invention of the ParameterizedThreadStart delegate (thankfully!) that's now support by the 2.0 Thread class.

Long story long, it's not the end of the world.  It's possible to work around it a few different ways.  But it's sure annoying to see this kind of design & implementation mistake this far into the history of callback-based APIs.


Posted Oct 21 2005, 01:59 PM by mike-woodring

Comments

Chris Sells wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 10-22-2005 8:24 AM
Possible to solve this problem using an anon delegate for the RunWorkerCompleted callback that pulls your user state from the constructing statck?
Some Assembly Required wrote Passing User State to a RunWorkerCompleted Handler
on 10-24-2005 11:40 AM
Some Assembly Required wrote Passing User State to a RunWorkerCompleted Handler
on 10-24-2005 11:47 AM
Some Assembly Required wrote Fun with ConstructorInfo
on 10-25-2005 3:10 PM
Luke Melia wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 10-31-2005 6:41 AM
Agreed. This is pretty lame.
TheChaseMan's Frenetic SoapBox wrote Fun with the BackgroundWorker Class
on 11-05-2005 11:59 PM
MetaMeta wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 04-06-2006 12:02 PM
Interesting blog, I wonder if you tried actually crashing your operation within the DoWork() and see if e.error actually works fine. I've been battling with it and it seems not to work : an exception thrown in the DoWork causes the app to stop with an 'Unhandled exception' error message, even if the handling is done in the WorkCompleted() method.
Deepak wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 05-30-2006 2:01 AM
what I have noticed is that an excpetion in the DoWork() handler stops the application . It does not get passed on to the RunWorkerCompleted() handler.
Kolik wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 06-22-2006 4:55 AM
Having unhandled exceptions with backgroundworker could point to a bad code in WorkCompleted event handler, causing an exception in it. Common mistake is to access e.Result when there has been an exception in DoWork event handler thrown. When e.Error is not null, you should not access e.Result.
Michael wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 06-28-2006 6:03 AM
From the MSDN Docs:

If the operation raises an exception that your code does not handle, the BackgroundWorker catches the exception and passes it into the RunWorkerCompleted event handler, where it is exposed as the Error property of System.ComponentModel.RunWorkerCompletedEventArgs. If you are running under the Visual Studio debugger, the debugger will break at the point in the DoWork event handler where the unhandled exception was raised.
mp035 wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 02-15-2007 6:26 PM
This is a newbie solution, but the result paramater of RunWorkerCompleted, and the argument parameter of DoWork are both objects, why not just pass userstate info through custom objects. See the link for an example. (sorry it's in vb, but you'll get the idea).
Doug wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 02-16-2007 8:25 AM
> Why not just pass userstate info through custom objects

His entire article was dedicated to the bug that the userstate info gets lost by the current implementation!
Peter wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 04-11-2007 9:11 AM
@ Kolik

>Common mistake is to access e.Result when there has been an exception in DoWork event handler thrown

Thank you! This was exactly the problem that I was having and seeing this let me track is down pretty quickly.
GeekTieGuy wrote re: BackgroundWorker/RunWorkerCompleteEventArgs bug
on 10-22-2007 2:20 PM
Maybe I'm missing something in the problem you ran into, but isn't the DoWorkEventsArgs.Result designed for passing state back to the RunWorkerCompletedEventHandler?

In other words

public void MyWorkerThreadFunction(object sender, DoWorkEventArgs e)
{
MyStateType myStateObject = e.Argument as MyStateType;

// do lengthy work here

e.Result = myStateObject;
}

would give you back the exact state object that was passed in.

Add a Comment

(required)  
(optional)
(required)  
Remember Me?