views:

6431

answers:

11

Consider a hypothetical method of an object that does stuff for you:

public class DoesStuff
{
   BackgroundWorker _worker = new BackgroundWorker();

   ...

   public void CancelDoingStuff()
   {
      _worker.CancelAsync();

     //todo: Figure out a way to wait for it to be cancelled
   }
}

How can one wait for a BackgroundWorker to be done? In the past people have tried:

   while (!_worker.IsDone)
   {
      Sleep(100);
   }

But this is a deadlock, since the IsBusy is not cleared until after the RunWorkerCompleted event is handled, and that event won't get handled until the application goes idle. (Plus, it's a busy loop - and how disgusting is that)

Others have add suggested kludging it into:

   while (!_worker.IsDone)
   {
      Application.DoEvents();
   }

Problem with that is that is Application.DoEvents() causes messages currently in the queue to be processed, which cause reentrantcy problems.

i would hope to use some solution involving Event synchronization objects, where the code waits for an event that the worker's RunWorkerCompleted event handlers sets, something like:

   Event _workerDoneEvent = new WaitHandle();

   public void CancelDoingStuff()
   {
      _worker.CancelAsync();

      _workerDoneEvent.WaitOne();
   }

   private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
   {
      _workerDoneEvent.SetEvent();
   }

But i'm back to the deadlock: the event handler can't run until the application goes idle, and the application won't go idle because it's waiting for an Event.

So how can you wait for an BackgroundWorker to finish?


Update People seem to be confused by this question. They seem to think that i will be using the BackgroundWorker as:

BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += MyWork;
worker.RunWorkerAsync();
WaitForWorkerToFinish(worker);

That is not it, that is not what i'm doing, and that is not what is being asked here. If that were the case, there would be no point in using a background worker.

+4  A: 

You can check into the RunWorkerCompletedEventArgs in the RunWorkerCompletedEventHandler to see what the status was. Success, canceled or an error.

private void RunWorkerCompletedEventHandler(sender object, RunWorkerCompletedEventArgs e)
{
    if(e.Cancelled)
    {
        Console.WriteLine("The worker was cancelled.");
    }
}

Update: To see if your worker has called .CancelAsync() by using this:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
Seb Nilsson
The requirement is that CancelDoingStuff() cannot return until the worker is completed. Checking how it completed really isn't of interest.
Ian Boyd
Then you will have to create an event. It really hasn't got anything to do with BackgroundWorker specifically, you just need to implement an event, listen for it and fire when done. And RunWorkerCompletedEventHandler is when it's done. Fire another event.
Seb Nilsson
+3  A: 

Why can't you just tie into the BackgroundWorker.RunWorkerCompleted Event. It's a callback that will "Occur when the background operation has completed, has been canceled, or has raised an exception."

Rick Minerich
Because the person who is using the DoesStuff object has asked it to cancel.Shared resources that the object uses are about to get disconnect, removed, disposed of, closed, and it needs to know that it's done so i can go ahead.
Ian Boyd
A: 

I don't understand why you'd want to wait for a BackgroundWorker to complete; it really seems like the exact opposite of the motivation for the class.

However, you could start every method with a call to worker.IsBusy and have them exit if it is running.

Austin Salonen
Bad choice of words; i'm not waiting for it to complete.
Ian Boyd
A: 

Hm maybe I am not getting your question right.

The backgroundworker calls the WorkerCompleted event once his 'workermethod' (the method/function/sub that handles the backgroundworker.doWork-event) is finished so there is no need for checking if the BW is still running. If you want to stop your worker check the cancellation pending property inside your 'worker method'.

Stephan
i understand that the worker has to monitor the CancellationPending property and exit as soon as possible.But how does the person on the outside, who has requested the background worker cancel, wait for it to be be done?
Ian Boyd
WorkCompleted event will still fire.
Joel Coehoorn
"But how does the person on the outside, who has requested the background worker cancel, wait for it to be done?"
Ian Boyd
+4  A: 

You don't wait for the background worker to complete. That pretty much defeats the purpose of launching a separate thread. Instead, you should let your method finish, and move any code that depends on completion to a different place. You let the worker tell you when it's done and call any remaining code then.

If you want to wait for something to complete use a different threading construct that provides a WaitHandle.

Joel Coehoorn
Can you suggest one? A background worker seems to be the only threading construct that can send notifications to the thread that constructed the BackgroundWorker object.
Ian Boyd
The backgroundworker is a _UI_ construct. It just uses a events that your own code still needs to know how to call. Nothing stopping you from creating your own delegates for that.
Joel Coehoorn
+15  A: 

If I understand your requirement right, you could do something like this (code not tested, but shows the general idea):

private BackgroundWorker worker = new BackgroundWorker();
private AutoResetEvent _resetEvent = new AutoResetEvent(false);

public Form1()
{
    InitializeComponent();

    worker.DoWork += worker_DoWork;
}

public void Cancel()
{
    worker.CancelAsync();
    _resetEvent.WaitOne(); // will block until _resetEvent.Set() call made
}

void worker_DoWork(object sender, DoWorkEventArgs e)
{
    while(!e.Cancel)
    {
        // do something
    }

    _resetEvent.Set(); // signal that worker is done
}
Fredrik Kalseth
That works great, except for one bug. The event needs to be created in the "not set" state: private AutoResetEvent _resetEvent = new AutoResetEvent(false);
Ian Boyd
So if you could edit your post and make that fix, it will be an answer fit for the ages!
Ian Boyd
Good catch; fixed :)
Fredrik Kalseth
This will block the UI (e.g. no repaints) if the background worker takes a long time to cancel. It's better to use one of the following to stop interaction with the UI: http://stackoverflow.com/questions/123661/net-how-to-wait-for-a-backgroundworker-to-complete#126570
Joe
+1 just what the doctor ordered...although I agree with @Joe if the cancel request can take more than a second.
dotjoe
+1  A: 

The workflow of a BackgroundWorker object basically requires you to handle the RunWorkerCompleted event for both normal execution and user cancellation use cases. This is why the property RunWorkerCompletedEventArgs.Cancelled exists. Basically, doing this properly requires that you consider your Cancel method to be an asynchronous method in itself.

Here's an example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;

namespace WindowsFormsApplication1
{
    public class AsyncForm : Form
    {
        private Button _startButton;
        private Label _statusLabel;
        private Button _stopButton;
        private MyWorker _worker;

        public AsyncForm()
        {
            var layoutPanel = new TableLayoutPanel();
            layoutPanel.Dock = DockStyle.Fill;
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.ColumnStyles.Add(new ColumnStyle());
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
            layoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

            _statusLabel = new Label();
            _statusLabel.Text = "Idle.";
            layoutPanel.Controls.Add(_statusLabel, 0, 0);

            _startButton = new Button();
            _startButton.Text = "Start";
            _startButton.Click += HandleStartButton;
            layoutPanel.Controls.Add(_startButton, 0, 1);

            _stopButton = new Button();
            _stopButton.Enabled = false;
            _stopButton.Text = "Stop";
            _stopButton.Click += HandleStopButton;
            layoutPanel.Controls.Add(_stopButton, 1, 1);

            this.Controls.Add(layoutPanel);
        }

        private void HandleStartButton(object sender, EventArgs e)
        {
            _stopButton.Enabled = true;
            _startButton.Enabled = false;

            _worker = new MyWorker() { WorkerSupportsCancellation = true };
            _worker.RunWorkerCompleted += HandleWorkerCompleted;
            _worker.RunWorkerAsync();

            _statusLabel.Text = "Running...";
        }

        private void HandleStopButton(object sender, EventArgs e)
        {
            _worker.CancelAsync();
            _statusLabel.Text = "Cancelling...";
        }

        private void HandleWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled)
            {
                _statusLabel.Text = "Cancelled!";
            }
            else
            {
                _statusLabel.Text = "Completed.";
            }

            _stopButton.Enabled = false;
            _startButton.Enabled = true;
        }

    }

    public class MyWorker : BackgroundWorker
    {
        protected override void OnDoWork(DoWorkEventArgs e)
        {
            base.OnDoWork(e);

            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread.Sleep(500);

                if (this.CancellationPending)
                {
                    e.Cancel = true;
                    e.Result = false;
                    return;
                }
            }

            e.Result = true;
        }
    }
}

If you really really don't want your method to exit, I'd suggest putting a flag like an AutoResetEvent on a derived BackgroundWorker, then override OnRunWorkerCompleted to set the flag. It's still kind of kludgy though; I'd recommend treating the cancel event like an asynchronous method and do whatever it's currently doing in the RunWorkerCompleted handler.

OwenP
Moving code to RunWorkerCompleted is not where it belongs, and is not pretty.
Ian Boyd
+3  A: 

There is a problem with this response. The UI needs to continue to process messages while you are waiting, otherwise it will not repaint, which will be a problem if your background worker takes a long time to respond to the cancel request.

One way to do this is to display a modal dialog which has a timer that repeatedly checks if the background worker has finished work (or finished cancelling in your case). Once the background worker has finished, the modal dialog returns control to your application. The user can't interact with the UI until this happens.

Another method (assuming you have a maximum of one modeless window open) is to set ActiveForm.Enabled = false, then loop on Application,DoEvents until the background worker has finished cancelling, after which you can set ActiveForm.Enabled = true again.

Joe
That may be a problem, but it is fundamentally accepted as part of the question, "How to **wait** for a BackgroundWorker to cancel". Waiting means you wait, you do nothing else. This also includes processing messages. If i didn't want to wait for the background worker you could just call .CancelAsync. But that isn't the design requirement here.
Ian Boyd
+1 for pointing out the value of the CancelAsync method, verses *waiting* for the background worker.
Ian Boyd
+1  A: 

Almost all of you are confused by the question, and are not understanding how a worker is used.

Consider a RunWorkerComplete event handler:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
            rocketOnPad = false;
     label1.Text = "Rocket launch complete";

    }
    else
    {
            rocketOnPad  = true;
     label1.Text = "Rocket launch aborted";
    }
    worker = null;
}

And all is good.

Now comes a situation where the caller needs to abort the countdown because they need to execute an emergency self-destruct of the rocket.

private void BlowUpRocket()
{
   if (worker != null)
   {
      worker.CancelAsync();
      WaitForWorkerToFinish(worker);
      worker = null;
   }

   StartClaxxon();
   SelfDestruct();
}

And there is also a situation where we need to open the access gates to the rocket, but not while doing a countdown:

private void OpenAccessGates()
{
   if (worker != null)
   {
      worker.CancelAsync();
      WaitForWorkerToFinish(worker);
      worker = null;
   }

   if (!rocketOnPad)
   {
      DisengateAllGateLatches();
   }
}

And finally, we need to un-fuel the rocket, but that's not allowed during a countdown:

private void DrainRocket()
{
   if (worker != null)
   {
     worker.CancelAsync();
     WaitForWorkerToFinish(worker);
     worker = null;
   }

   if (rocketOnPad)
      OpenFuelValves();
}

Without the ability to wait for a worker to cancel, we must move all three methods to the RunWorkerCompletedEvent:

private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (!e.Cancelled)
    {
            rocketOnPad = false;
            label1.Text = "Rocket launch complete";

    }
    else
    {
            rocketOnPad  = true;
            label1.Text = "Rocket launch aborted";
    }
      worker = null;

    if (delayedBlowUpRocket)
       BlowUpRocket();
    else if (delayedOpenAccessGates)
       OpenAccessGates();
    else if (delayedDrainRocket)
       DrainRocket();
}

private void BlowUpRocket()
{
   if (worker != null)
   {
      delayedBlowUpRocket = true;
      worker.CancelAsync();
      return;
   }

   StartClaxxon();
   SelfDestruct();
}

private void OpenAccessGates()
{
   if (worker != null)
   {
      delayedOpenAccessGates = true;
      worker.CancelAsync();
      return;
   }

   if (!rocketOnPad)
   {
      DisengateAllGateLatches();
   }
}

private void DrainRocket()
{
   if (worker != null)
   {
     delayedDrainRocket = true;
     worker.CancelAsync();
     return;
   }

   if (rocketOnPad)
      OpenFuelValves();
}

Now i could write my code like that, but i'm just not gonna. i don't care, i'm just not.

Ian Boyd
A: 

oh man, some of these have gotten ridiculously complex. all you need to do is check the BackgroundWorker.CancellationPending property inside the DoWork handler. you can check it at any time. once it's pending, set e.Cancel = True and bail from the method.

// method here private void Worker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bw = (sender as BackgroundWorker);

// do stuff

if(bw.CancellationPending)
{
    e.Cancel = True;
    return;
}

// do other stuff

}

And with this solution how is the person who called CancelAsync forced to wait for the background worker to cancel?
Ian Boyd
+2  A: 

I understand the question, I have the same issue myself, it would be nice to get the thread that the background worker is using and doing a Join on it.

That is exactly the semantic i want, a Join.
Ian Boyd