如何等待BackgroundWorker取消?

时间:2008-09-23 20:30:50

标签: c# .net multithreading backgroundworker

考虑一个对象的假设方法,它为你做的事情:

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

但是this deadlocks,因为IsBusy直到处理完RunWorkerCompleted事件后才被清除,并且在应用程序空闲之前无法处理该事件。在工人完成之前,应用程序不会闲置。 (另外,这是一个繁忙的循环 - 恶心。)

其他人已经建议将其改为:

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

那是它,我在做什么,而这里要问的是什么。如果是这种情况,那么使用后台工作者是没有意义的。

17 个答案:

答案 0 :(得分:121)

如果我理解你的要求是正确的,你可以做这样的事情(代码没有经过测试,但显示了一般的想法):

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
}

答案 1 :(得分:13)

this响应存在问题。 UI需要在您等待时继续处理消息,否则它将不会重新绘制,如果您的后台工作程序需要很长时间来响应取消请求,这将是一个问题。

第二个缺陷是,如果工作线程抛出异常,永远不会调用_resetEvent.Set() - 让主线程无限期地等待 - 但是这个缺陷可以通过try / finally块轻松修复。

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

另一种方法(假设您最多打开一个无模式窗口)是设置ActiveForm.Enabled = false,然后在Application,DoEvents上循环,直到后台工作程序完成取消,之后您可以设置ActiveForm.Enabled = true试。

答案 2 :(得分:10)

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

考虑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();
}

如果没有等待worker取消的能力,我们必须将所有三种方法都移到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();
}

现在我可以像这样写我的代码,但我不会。我不在乎,我只是没有。

答案 3 :(得分:4)

您可以访问 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");
}

答案 4 :(得分:4)

等待后台工作人员完成。这几乎打败了启动单独线程的目的。相反,您应该让您的方法完成,并将依赖于完成的任何代码移动到其他位置。你让工作人员告诉你什么时候完成,然后调用任何剩余的代码。

如果你想等待完成某些事情,请使用另一个提供WaitHandle的线程结构。

答案 5 :(得分:3)

为什么你不能只是绑定到BackgroundWorker.RunWorkerCompleted事件。这是一个回调,它将“在后台操作完成,被取消或引发异常时发生。”

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

只是想说我来到这里是因为我需要一个后台工作人员等待我在循环中运行异步进程时,我的修复比其他所有东西更容易^^

XMLHttpRequest

只是想到我分享,因为这是我在搜索解决方案时最终的结果。另外,这是我关于堆栈溢出的第一篇文章,所以如果它不好或任何我喜欢评论家! :)

答案 8 :(得分:0)

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

如果真的不希望你的方法退出,我建议在派生的AutoResetEvent上添加BackgroundWorker之类的标记,然后覆盖{{ 1}}设置标志。尽管如此,它仍然是一种kludgy;我建议像对待异步方法一样处理cancel事件,并在OnRunWorkerCompleted处理程序中执行它当前正在做的任何事情。

答案 9 :(得分:0)

嗯,也许我没有问你的问题。

一旦他的'workermethod'(处理backgroundworker.doWork-event的方法/函数/子)完成,后台工作者调用WorkerCompleted事件,因此不需要检查BW是否仍在运行。 如果你想让你的工作人员检查你的'工人方法'中的cancellation pending property

答案 10 :(得分:0)

我在这里参加派对有点晚了(大约4年)但是如何设置一个可以在不锁定UI的情况下处理繁忙循环的异步线程,然后让该线程的回调成为BackgroundWorker的确认已经取消了吗?

这样的事情:

class Test : Form
{
    private BackgroundWorker MyWorker = new BackgroundWorker();

    public Test() {
        MyWorker.DoWork += new DoWorkEventHandler(MyWorker_DoWork);
    }

    void MyWorker_DoWork(object sender, DoWorkEventArgs e) {
        for (int i = 0; i < 100; i++) {
            //Do stuff here
            System.Threading.Thread.Sleep((new Random()).Next(0, 1000));  //WARN: Artificial latency here
            if (MyWorker.CancellationPending) { return; } //Bail out if MyWorker is cancelled
        }
    }

    public void CancelWorker() {
        if (MyWorker != null && MyWorker.IsBusy) {
            MyWorker.CancelAsync();
            System.Threading.ThreadStart WaitThread = new System.Threading.ThreadStart(delegate() {
                while (MyWorker.IsBusy) {
                    System.Threading.Thread.Sleep(100);
                }
            });
            WaitThread.BeginInvoke(a => {
                Invoke((MethodInvoker)delegate() { //Invoke your StuffAfterCancellation call back onto the UI thread
                    StuffAfterCancellation();
                });
            }, null);
        } else {
            StuffAfterCancellation();
        }
    }

    private void StuffAfterCancellation() {
        //Things to do after MyWorker is cancelled
    }
}

实质上,这样做是触发另一个在后台运行的线程,它只是在忙碌的循环中等待,看看MyWorker是否已经完成。一旦MyWorker完成取消,线程将退出,我们可以使用它AsyncCallback执行我们需要的任何方法来成功取消 - 它将像伪事件一样工作。由于这与UI线程是分开的,因此当我们等待MyWorker完成取消时,它不会锁定UI。如果你的意图是锁定并等待取消,那么这对你来说没用,但是如果你只是想等待,那么你可以开始另一个过程,那么这很有效。

答案 11 :(得分:0)

Imports System.Net
Imports System.IO
Imports System.Text

Public Class Form1
   Dim f As New Windows.Forms.Form
  Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
   BackgroundWorker1.WorkerReportsProgress = True
    BackgroundWorker1.RunWorkerAsync()
    Dim l As New Label
    l.Text = "Please Wait"
    f.Controls.Add(l)
    l.Dock = DockStyle.Fill
    f.StartPosition = FormStartPosition.CenterScreen
    f.FormBorderStyle = Windows.Forms.FormBorderStyle.None
    While BackgroundWorker1.IsBusy
        f.ShowDialog()
    End While
End Sub




Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim i As Integer
    For i = 1 To 5
        Threading.Thread.Sleep(5000)
        BackgroundWorker1.ReportProgress((i / 5) * 100)
    Next
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    Me.Text = e.ProgressPercentage

End Sub

 Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

    f.Close()

End Sub

End Class

答案 12 :(得分:0)

我知道这已经很晚了(5年),但你要找的是使用一个主题和一个SynchronizationContext。您将不得不“手动”将UI调用回调到UI线程,而不是让框架自动执行它。

这允许您使用可以在需要时等待的线程。

答案 13 :(得分:0)

Fredrik Kalseth对这个问题的解决方案是迄今为止我发现的最好的解决方案。其他解决方案使用Application.DoEvent()可能会导致问题或根本不起作用。让我把他的解决方案变成一个可重用的类。由于BackgroundWorker未被封存,我们可以从中派生出类:

public class BackgroundWorkerEx : BackgroundWorker
{
    private AutoResetEvent _resetEvent = new AutoResetEvent(false);
    private bool _resetting, _started;
    private object _lockObject = new object();

    public void CancelSync()
    {
        bool doReset = false;
        lock (_lockObject) {
            if (_started && !_resetting) {
                _resetting = true;
                doReset = true;
            }
        }
        if (doReset) {
            CancelAsync();
            _resetEvent.WaitOne();
            lock (_lockObject) {
                _started = false;
                _resetting = false;
            }
        }
    }

    protected override void OnDoWork(DoWorkEventArgs e)
    {
        lock (_lockObject) {
            _resetting = false;
            _started = true;
            _resetEvent.Reset();
        }
        try {
            base.OnDoWork(e);
        } finally {
            _resetEvent.Set();
        }
    }
}

使用标记和正确锁定,我们确保_resetEvent.WaitOne()只在某些工作开始时才被调用,否则_resetEvent.Set();可能永远不会被调用!

try-finally确保将调用_resetEvent.Set();,即使在我们的DoWork-handler中发生异常也是如此。否则,在调用CancelSync时,应用程序可能会永远冻结!

我们会这样使用它:

BackgroundWorkerEx _worker;

void StartWork()
{
    StopWork();
    _worker = new BackgroundWorkerEx { 
        WorkerSupportsCancellation = true,
        WorkerReportsProgress = true
    };
    _worker.DoWork += Worker_DoWork;
    _worker.ProgressChanged += Worker_ProgressChanged;
}

void StopWork()
{
    if (_worker != null) {
        _worker.CancelSync(); // Use our new method.
    }
}

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    for (int i = 1; i <= 20; i++) {
        if (worker.CancellationPending) {
            e.Cancel = true;
            break;
        } else {
            // Simulate a time consuming operation.
            System.Threading.Thread.Sleep(500);
            worker.ReportProgress(5 * i);
        }
    }
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressLabel.Text = e.ProgressPercentage.ToString() + "%";
}

您还可以为RunWorkerCompleted事件添加处理程序,如下所示:
BackgroundWorker Class (Microsoft文档)

答案 14 :(得分:0)

关闭表单将关闭我的打开日志文件。我的后台工作者写了那个日志文件,所以在我的后台工作者终止之前我不能让MainWin_FormClosing()完成。如果我不等待后台工作者终止,则会发生异常。

为什么这么难?

一个简单的Thread.Sleep(1500)可以正常工作,但会延迟关闭(如果太长),或导致异常(如果太短)。

要在后台工作程序终止后立即关闭,只需使用变量即可。这对我有用:

private volatile bool bwRunning = false;

...

private void MainWin_FormClosing(Object sender, FormClosingEventArgs e)
{
    ... // Clean house as-needed.

    bwInstance.CancelAsync();  // Flag background worker to stop.
    while (bwRunning)
        Thread.Sleep(100);  // Wait for background worker to stop.
}  // (The form really gets closed now.)

...

private void bwBody(object sender, DoWorkEventArgs e)
{
    bwRunning = true;

    BackgroundWorker bw = sender as BackgroundWorker;

    ... // Set up (open logfile, etc.)

    for (; ; )  // infinite loop
    {
        ...
        if (bw.CancellationPending) break;
        ...
    } 

    ... // Tear down (close logfile, etc.)

    bwRunning = false;
}  // (bwInstance dies now.)

答案 15 :(得分:0)

我使用async方法和await等待工人完成工作:

    public async Task StopAsync()
    {
        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);
    }

并使用DoWork方法:

    public async Task DoWork()
    {
        _isBusy = true;
        while (!_worker.CancellationPending)
        {
            // Do something.
        }
        _isBusy = false;
    }

您也可以将while的{​​{1}}循环封装在DoWork中,以将try ... catch设置为_isBusy例外。或者,只需在false while循环中检查_worker.IsBusy

以下是完整实施的示例:

StopAsync

要停止工作程序并等待其结束:

class MyBackgroundWorker
{
    private BackgroundWorker _worker;
    private bool _isBusy;

    public void Start()
    {
        if (_isBusy)
            throw new InvalidOperationException("Cannot start as a background worker is already running.");

        InitialiseWorker();
        _worker.RunWorkerAsync();
    }

    public async Task StopAsync()
    {
        if (!_isBusy)
            throw new InvalidOperationException("Cannot stop as there is no running background worker.");

        _worker.CancelAsync();

        while (_isBusy)
            await Task.Delay(1);

        _worker.Dispose();
    }

    private void InitialiseWorker()
    {
        _worker = new BackgroundWorker
        {
            WorkerSupportsCancellation = true
        };
        _worker.DoWork += WorkerDoWork;
    }

    private void WorkerDoWork(object sender, DoWorkEventArgs e)
    {
        _isBusy = true;
        try
        {
            while (!_worker.CancellationPending)
            {
                // Do something.
            }
        }
        catch
        {
            _isBusy = false;
            throw;
        }

        _isBusy = false;
    }
}

此方法的问题是:

  1. 您必须一直使用异步方法。
  2. 等待任务。延迟不正确。在我的PC上,Task.Delay(1)实际上等待约20毫秒。

答案 16 :(得分:-2)

哦,男人,其中一些变得非常复杂。您需要做的就是检查DoWork处理程序中的BackgroundWorker.CancellationPending属性。你可以随时查看。一旦它挂起,设置e.Cancel = True并从该方法保释。

//方法在这里 private void Worker_DoWork(对象发送者,DoWorkEventArgs e) {     BackgroundWorker bw =(发件人为BackgroundWorker);

// do stuff

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

// do other stuff

}

相关问题