views:

232

answers:

4

I'm using a BackgroundWorker to perform a long computation (only one such computation at a time).

The user has the possibility of cancelling the worker (calling worker.CancelAsync).

In the worker.DoWork method I periodically check for the cancel pending flag and then return from the method.

Then the Completed event is raised from the worker and I can check that the worker was cancelled. Furthermore, and that is the important thing, I do some extra cleanup when a cancel is detected.

I am sure there could be problem if the user cancels the worker and it's already returned from the DoWork method. In that case I would really like to know that the worker was cancelled so I could cleanup...

Is there a better way to handle the cancel procedure, with cleanup, of a worker ?

A: 

Is there a reason you can't do the cleanup at the end of the method you run asynchronously?

I generally use a structure like this

private void MyThreadMethod()
{
    try
    {
        // Thread code
    }
    finally 
    {
        // Cleanup
    }
}

I have only use the complete event to do tasks like update the UI, not to clean up after the thread.

Eric J.
Yeah, I cannot do the cleanup in the background worker... I need to do it on the UI thread.
Stecy
Are you seeing the event fire more than once? If so, you can use a lock(...) {} block in your event handler to ensure the code can only be executed once at a time, and set a flag indicating that cleanup is done (check that flag first thing in your lock block and only run cleanup code if not set).
Eric J.
A: 

Without seeing your code it's a little hard to make suggestions, but one thing you could do is disable the "Cancel" button when the user clicks it and also on exit from the DoWork method.

If the user can also cancel via a key press then you'll also need to disable that too.

Also - if the DoWork method has finished then there's nothing for the user to "Cancel" - or am I missing something. In that case even if you don't disable the cancel button there's nothing extra you need to do.

Why do you need to see the cancel flag after the worker has done its job? Do you still want to roll back what ever was changed?

ChrisF
In fact, the problem is not preventing the user canceling multiple times... The problem is more of making sure I see the cancel flag even AFTER the worker has done its job and the Completed event is received.
Stecy
+1  A: 

You are guaranteed not to have a race in the RunWorkerCompleted event handler since it runs on the UI thread. This should always work:

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
  var bgw = sender as BackgroundWorker;
  if (e.Cancelled || bgw.CancellationPending) {
    // etc..
  }
}
Hans Passant
+2  A: 

Your DoWork event handler shoud periodically check BackgroundWorker.CancellationPending, and set DoWorkEventArgs.Cancel to true before returning if it was cancelled.

Your RunWorkerCompleted event handler should check the RunWorkerCompletedEventArgs.Cancelled property to determine if the DoWork event handler cancelled (set DoWorkEventArgs.Cancel to true).

In the event of a race condition, it may happen that the user requested cancellation (BackgroundWorker.CancellationPending is true) but the worker didn't see it (RunWorkerCompletedEventArgs.Cancelled is false). You can test these two properties to determine that this has occurred, and do whatever you choose (either treat it as successful completion - because the worker did actually finish successfully, or as a cancellation - because the user has cancelled and doesn't care any more).

I don't see any situation where there is any ambiguity about what happened.

EDIT

In response to the comment - if there are several classes that need to detect CancellationPending, there's no really no alternative to passing to these classes a reference to a type such as BackgroundWorker that allows them to retrieve this information. You can abstract this into an interface, which is what I generally do as described in this response to a question about BackgroundWorkers. But you still need to pass a reference to a type that implements this interface to your worker classes.

If you want your worker classes to be able to set DoWorkEventArgs.Cancel you will need to either pass a reference to this around, or adopt a different convention (e.g. a boolean return value or custom exception) that allows your worker classes to indicate that cancellation has occurred.

Joe
That's a really clear explanation. However, since the work is not done solely in the "DoWork" method, I would have to pass the EventArgs around to every class that would check for cancellation and I have around 30 of those classes (deep optimization problem). This is feasible but I feel that would be a PITA... Also I don't like those classes having to manage cancellation. I'm still looking for a better way.
Stecy
"Also I don't like those classes having to manage cancellation" - I don't understand your concern. If so, don't have them manage cancellation! Instead, only check for cancellation whenever one of these classes returns control to your main DoWork method.
Joe
Those inner classes are doing a really long computation and therefore they do not return often.
Stecy