如何等待BackgroundWorker取消?

时间:2020-03-06 14:37:31  来源:igfitidea点击:

考虑一个为我们做事的对象的假设方法:

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

    ...

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

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

如何等待BackgroundWorker完成?

过去人们尝试过:

while (_worker.IsBusy)
{
    Sleep(100);
}

但这会造成僵局,因为直到处理完RunWorkerCompleted事件之后才会清除IsBusy,并且直到应用程序空闲之后才能处理该事件。直到工作人员完成后,应用程序才会处于空闲状态。 (此外,这是一个令人讨厌的繁忙循环。)

其他人则建议将其合并为:

while (_worker.IsBusy)
{
    Application.DoEvents();
}

问题在于Application.DoEvents()导致当前队列中的消息被处理,从而导致重新输入问题(.NET不能重新输入)。

我希望使用一些涉及事件同步对象的解决方案,其中代码等待工作者的RunWorkerCompleted事件处理程序设置的事件。就像是:

Event _workerDoneEvent = new WaitHandle();

public void CancelDoingStuff()
{
    _worker.CancelAsync();
    _workerDoneEvent.WaitOne();
}

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

但是我又回到了僵局:事件处理程序要等到应用程序空闲后才能运行,并且应用程序也不会因为等待事件而处于空闲状态。

那么,如何等待BackgroundWorker完成呢?

更新
人们似乎对这个问题感到困惑。他们似乎认为我将使用BackgroundWorker作为:

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

就是这样,这不是我在做什么,这不是这里要问的。如果真是这样,那么使用后台工作人员将毫无意义。

解决方案

我们可以在RunWorkerCompletedEventHandler中检查RunWorkerCompletedEventArgs,以查看其状态。成功,已取消或者错误。

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

更新:使用以下命令查看工作人员是否已调用.CancelAsync():

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}

我们为什么不能只绑定到BackgroundWorker.RunWorkerCompleted事件。这是一个回调,将"在后台操作完成,已取消或者引发异常时发生。"

我不明白我们为什么要等待BackgroundWorker完成;看来,这与上课动机完全相反。

但是,我们可以通过调用worker.IsBusy来启动每个方法,并在运行时退出它们。

嗯,也许我没能正确回答问题。

后台工作人员的"工作方法"(处理backgroundworker.doWork-event的方法/函数/子程序)完成后,将调用WorkerCompleted事件,因此无需检查BW是否仍在运行。
如果要停止工作,请检查"工作人员方法"中的取消待处理属性。

我们无需等待后台工作人员完成。这几乎违反了启动单独线程的目的。相反,我们应该让方法完成,然后将依赖于完成的所有代码移动到其他位置。我们让工作人员告诉我们完成的时间,然后调用所有剩余的代码。

如果要等待完成,请使用提供WaitHandle的其他线程构造。

如果我了解要求正确,则可以执行以下操作(代码未经测试,但显示了总体思路):

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
}

对于常规执行和用户取消用例," BackgroundWorker"对象的工作流程基本上要求我们处理" RunWorkerCompleted"事件。这就是为什么存在属性RunWorkerCompletedEventArgs.Cancelled的原因。基本上,正确执行此操作要求我们将Cancel方法本身视为异步方法。

这是一个例子:

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;
        }
    }
}

如果我们确实不希望退出该方法,建议我们在派生的BackgroundWorker上放置一个类似于AutoResetEvent的标志,然后重写OnRunWorkerCompleted来设置该标志。但是,这仍然有点糊涂。我建议将cancel事件视为一个异步方法,并执行RunWorkerCompleted处理程序中当前正在执行的任何操作。

此响应有问题。 UI需要在等待时继续处理消息,否则它将不会重新绘制,如果后台工作人员花很长时间响应取消请求,这将是一个问题。

第二个缺陷是,如果工作线程引发异常而使主线程无限期地等待,则永远不会调用_resetEvent.Set(),但是可以使用try / finally块轻松修复该缺陷。

一种方法是显示一个模态对话框,其中包含一个计时器,该计时器可反复检查后台工作人员是否已完成工作(或者情况下完成取消)。后台工作人员完成操作后,模态对话框将控制权返回给应用程序。在这种情况发生之前,用户无法与UI进行交互。

另一种方法(假设我们最多打开了一个无模式窗口)是将ActiveForm.Enabled = false设置为,然后在Application,DoEvents上循环运行,直到后台工作人员完成取消为止,然后可以再次将ActiveForm.Enabled = true设置为。

几乎所有人都对这个问题感到困惑,并且不了解如何使用工人。

考虑一个RunWorkerComplete事件处理程序:

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;
}

一切都很好。

现在出现了这样一种情况,即呼叫者需要中止倒计时,因为他们需要执行火箭的紧急自毁。

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

    StartClaxon();
    SelfDestruct();
}

还有一种情况,我们需要打开火箭的检修门,而不是在倒数计时时:

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

最后,我们需要给火箭加油,但是在倒数计时中这是不允许的:

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

    if (rocketOnPad)
        OpenFuelValves();
}

没有等待工人取消的能力,我们必须将所有三个方法移至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;
    }

    StartClaxon();
    SelfDestruct();
}

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

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

    if (rocketOnPad)
        OpenFuelValves();
}

现在,我可以这样编写代码,但是我不会。我不在乎,我只是不在乎。

哦,伙计,其中有些变得异常复杂。我们需要做的就是检查DoWork处理程序中的BackgroundWorker.CancellationPending属性。我们可以随时检查。待处理后,设置e.Cancel = True并从该方法中保释。

//这里的方法
私有void Worker_DoWork(对象发送者,DoWorkEventArgs e)
{
BackgroundWorker bw =(发送者为BackgroundWorker);

// do stuff

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

// do other stuff

}