Wie kann ich warten, bis ein BackgroundWorker abbricht?

125

Stellen Sie sich eine hypothetische Methode eines Objekts vor, das Dinge für Sie erledigt:

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

    ...

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

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

Wie kann man warten, bis ein BackgroundWorker fertig ist?


In der Vergangenheit haben die Leute versucht:

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

Aber diese Deadlocks , da IsBusywird nicht gelöscht , bis das RunWorkerCompletedEreignis behandelt wird, und das Ereignis nicht behandelt werden , bis die Anwendung im Leerlauf geht. Die Anwendung wird erst dann inaktiv, wenn der Worker fertig ist. (Außerdem ist es eine belebte Schleife - widerlich.)

Andere haben vorgeschlagen, es zu kludgen in:

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

Das Problem dabei ist, Application.DoEvents()dass Nachrichten, die sich derzeit in der Warteschlange befinden, verarbeitet werden, was zu Problemen beim erneuten Eintritt führt (.NET wird nicht erneut eingegeben).

Ich würde hoffen, eine Lösung mit Ereignissynchronisationsobjekten zu verwenden, bei der der Code auf ein Ereignis wartet - das die RunWorkerCompletedEreignishandler des Arbeiters festlegen. Etwas wie:

Event _workerDoneEvent = new WaitHandle();

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

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

Aber ich bin zurück zum Deadlock: Der Ereignishandler kann erst ausgeführt werden, wenn die Anwendung inaktiv ist, und die Anwendung wird nicht inaktiv, weil sie auf ein Ereignis wartet.

Wie können Sie also warten, bis ein BackgroundWorker fertig ist?


Update Die Leute scheinen von dieser Frage verwirrt zu sein. Sie scheinen zu glauben, dass ich den BackgroundWorker verwenden werde als:

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

Das ist es nicht , das ist nicht was ich tue und das ist nicht was hier gefragt wird. Wenn dies der Fall wäre, wäre es sinnlos, einen Hintergrundarbeiter einzusetzen.

Ian Boyd
quelle

Antworten:

130

Wenn ich Ihre Anforderung richtig verstehe, können Sie so etwas tun (Code nicht getestet, zeigt aber die allgemeine Idee):

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
quelle
7
Dadurch wird die Benutzeroberfläche blockiert (z. B. keine Neulackierungen), wenn der Abbruch des Hintergrundarbeiters lange dauert. Es ist besser, eine der folgenden Optionen
Joe
1
+1 genau das, was der Arzt bestellt hat ... obwohl ich @Joe zustimme, wenn die Stornierungsanfrage länger als eine Sekunde dauern kann.
Dotjoe
Was passiert, wenn CancelAsync vor WaitOne verarbeitet wird? Oder funktioniert die Schaltfläche Abbrechen nur einmal?
CodingBarfield
6
Ich musste das überprüfen ((BackgroundWorker)sender).CancellationPending, um das Stornierungsereignis zu erhalten
Luuk
1
Wie von Luuk erwähnt, muss nicht die Cancel-Eigenschaft überprüft werden, sondern CancellationPending. Zweitens, wie Joel Coehoorn erwähnt, vereitelt das Warten auf das Beenden des Threads den Zweck, überhaupt einen Thread zu verwenden.
Captain Sensible
15

Bei dieser Antwort liegt ein Problem vor . Die Benutzeroberfläche muss weiterhin Nachrichten verarbeiten, während Sie warten. Andernfalls wird sie nicht neu gestrichen. Dies ist ein Problem, wenn Ihr Hintergrundmitarbeiter lange braucht, um auf die Abbruchanforderung zu antworten.

Ein zweiter Fehler ist, dass er _resetEvent.Set()niemals aufgerufen wird, wenn der Worker-Thread eine Ausnahme auslöst - der Haupt-Thread bleibt unbegrenzt warten -, dieser Fehler kann jedoch leicht mit einem try / finally-Block behoben werden.

Eine Möglichkeit, dies zu tun, besteht darin, ein modales Dialogfeld mit einem Timer anzuzeigen, der wiederholt prüft, ob der Hintergrundarbeiter die Arbeit beendet hat (oder in Ihrem Fall den Abbruch beendet hat). Sobald der Hintergrund-Worker fertig ist, gibt der modale Dialog die Kontrolle an Ihre Anwendung zurück. Der Benutzer kann erst dann mit der Benutzeroberfläche interagieren.

Eine andere Methode (vorausgesetzt, Sie haben maximal ein modellloses Fenster geöffnet) besteht darin, ActiveForm.Enabled = false festzulegen und dann die Anwendung DoEvents zu durchlaufen, bis der Hintergrund-Worker den Abbruch abgeschlossen hat. Anschließend können Sie ActiveForm.Enabled = true erneut festlegen.

Joe
quelle
5
Das mag ein Problem sein, wird aber grundsätzlich als Teil der Frage "Wie man darauf wartet, dass ein BackgroundWorker abbricht" akzeptiert . Warten bedeutet, dass Sie warten, Sie tun nichts anderes. Dies umfasst auch die Verarbeitung von Nachrichten. Wenn ich nicht auf den Hintergrundarbeiter warten wollte, konnte ich einfach .CancelAsync aufrufen. Aber das ist hier nicht die Designanforderung.
Ian Boyd
1
+1 für den Hinweis auf den Wert der CancelAsync-Methode, Verse, die auf den Hintergrundarbeiter warten .
Ian Boyd
10

Fast alle von Ihnen sind verwirrt von der Frage und verstehen nicht, wie ein Arbeiter eingesetzt wird.

Betrachten Sie einen RunWorkerComplete-Ereignishandler:

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

Und alles ist gut.

Jetzt kommt eine Situation, in der der Anrufer den Countdown abbrechen muss, weil er eine Selbstzerstörung der Rakete im Notfall ausführen muss.

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

    StartClaxon();
    SelfDestruct();
}

Und es gibt auch eine Situation, in der wir die Zugangstore zur Rakete öffnen müssen, aber nicht während eines Countdowns:

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

Und schließlich müssen wir die Rakete tanken, aber das ist während eines Countdowns nicht erlaubt:

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

    if (rocketOnPad)
        OpenFuelValves();
}

Ohne die Möglichkeit, auf das Abbrechen eines Workers zu warten, müssen alle drei Methoden in das RunWorkerCompletedEvent verschoben werden:

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

Jetzt könnte ich meinen Code so schreiben, aber ich werde es einfach nicht tun. Es ist mir egal, ich bin es einfach nicht.

Ian Boyd
quelle
18
Wo ist die WaitForWorkerToFinish-Methode? irgendeinen vollständigen Quellcode?
Kiquenet
4

Sie können in den RunWorkerCompletedEventArgs im RunWorkerCompletedEventHandler nachsehen , wie der Status war. Erfolg, abgebrochen oder ein Fehler.

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

Update : Um zu sehen, ob Ihr Worker .CancelAsync () aufgerufen hat, verwenden Sie Folgendes:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
Seb Nilsson
quelle
2
Voraussetzung ist, dass CancelDoingStuff () erst nach Abschluss des Workers zurückgegeben werden kann. Zu überprüfen, wie es wirklich abgeschlossen wurde, ist nicht von Interesse.
Ian Boyd
Dann müssen Sie ein Ereignis erstellen. Es hat wirklich nichts speziell mit BackgroundWorker zu tun. Sie müssen nur ein Ereignis implementieren, darauf warten und abfeuern, wenn Sie fertig sind. Und RunWorkerCompletedEventHandler ist fertig. Feuern Sie ein anderes Ereignis ab.
Seb Nilsson
4

Sie warten nicht auf den Abschluss des Hintergrundarbeiters. Das macht den Zweck, einen separaten Thread zu starten, ziemlich zunichte. Stattdessen sollten Sie Ihre Methode beenden lassen und jeden Code, der von der Fertigstellung abhängt, an einen anderen Ort verschieben. Sie lassen sich vom Mitarbeiter sagen, wann es fertig ist, und rufen dann den verbleibenden Code auf.

Wenn Sie warten möchten, bis etwas abgeschlossen ist, verwenden Sie ein anderes Threading-Konstrukt, das einen WaitHandle bereitstellt.

Joel Coehoorn
quelle
1
Können Sie einen vorschlagen? Ein Hintergrund-Worker scheint das einzige Threading-Konstrukt zu sein, das Benachrichtigungen an den Thread senden kann, der das BackgroundWorker-Objekt erstellt hat.
Ian Boyd
Der Hintergrundarbeiter ist ein UI- Konstrukt. Es werden nur Ereignisse verwendet, die Ihr eigener Code noch aufrufen muss. Nichts hindert Sie daran, Ihre eigenen Delegierten dafür zu erstellen.
Joel Coehoorn
3

Warum können Sie sich nicht einfach an das BackgroundWorker.RunWorkerCompleted-Ereignis binden? Es handelt sich um einen Rückruf, der "auftritt, wenn der Hintergrundvorgang abgeschlossen, abgebrochen oder eine Ausnahme ausgelöst wurde".

Rick Minerich
quelle
1
Weil die Person, die das DoesStuff-Objekt verwendet, es zum Abbrechen aufgefordert hat. Freigegebene Ressourcen, die das Objekt verwendet, werden bald getrennt, entfernt, entsorgt, geschlossen, und es muss wissen, dass dies erledigt ist, damit ich fortfahren kann.
Ian Boyd
1

Ich verstehe nicht, warum Sie auf den Abschluss eines BackgroundWorker warten möchten. es scheint wirklich das genaue Gegenteil der Motivation für die Klasse zu sein.

Sie können jedoch jede Methode mit einem Aufruf von worker.IsBusy starten und sie beenden lassen, wenn sie ausgeführt wird.

Austin Salonen
quelle
Schlechte Wortwahl; Ich warte nicht darauf, dass es fertig ist.
Ian Boyd
1

Ich möchte nur sagen, dass ich hierher gekommen bin, weil ich einen Hintergrundarbeiter brauche, der wartet, während ich einen asynchronen Prozess in einer Schleife ausführe. Mein Fix war viel einfacher als all diese anderen Dinge ^^

foreach(DataRow rw in dt.Rows)
{
     //loop code
     while(!backgroundWorker1.IsBusy)
     {
         backgroundWorker1.RunWorkerAsync();
     }
}

Ich dachte nur, ich würde teilen, weil ich hier auf der Suche nach einer Lösung gelandet bin. Dies ist auch mein erster Beitrag zum Stapelüberlauf. Wenn es also schlecht ist oder irgendetwas, würde ich Kritiker lieben! :) :)

Connor Williams
quelle
0

Hm, vielleicht verstehe ich deine Frage nicht richtig.

Der Hintergrundarbeiter ruft das WorkerCompleted-Ereignis auf, sobald seine 'Arbeitsmethode' (die Methode / Funktion / Sub, die das Hintergrundworker.doWork-Ereignis behandelt ) abgeschlossen ist, sodass nicht überprüft werden muss, ob das BW noch ausgeführt wird. Wenn Sie Ihren Worker stoppen möchten, überprüfen Sie die Eigenschaft "Stornierung anstehend" in Ihrer "Worker-Methode".

Stephan
quelle
Ich verstehe, dass der Arbeiter die CancellationPending-Eigenschaft überwachen und so schnell wie möglich beenden muss. Aber wie wartet die Person von außen, die den Hintergrundarbeiter um Stornierung gebeten hat, darauf, dass dies getan wird?
Ian Boyd
Das WorkCompleted-Ereignis wird weiterhin ausgelöst.
Joel Coehoorn
"Aber wie wartet die Person von außen, die den Hintergrundarbeiter gebeten hat abzusagen, darauf, dass dies getan wird?"
Ian Boyd
Sie warten darauf, dass dies ausgeführt wird, indem Sie darauf warten, dass das WorkCompleted-Ereignis ausgelöst wird. Wenn Sie verhindern möchten, dass der Benutzer auf Ihrer gesamten Benutzeroberfläche eine Klickanpassung hat, können Sie eine der von @Joe in seiner obigen Antwort vorgeschlagenen Lösungen verwenden (deaktivieren Sie das Formular oder zeigen Sie etwas Modales an ). Mit anderen Worten, lassen Sie die Leerlaufschleife des Systems auf Sie warten. Es wird Ihnen sagen, wann es beendet ist (durch Auslösen des Ereignisses).
Geoff
0

Der Workflow eines BackgroundWorkerObjekts erfordert grundsätzlich, dass Sie das RunWorkerCompletedEreignis sowohl für Anwendungsfälle mit normaler Ausführung als auch für Benutzerabbruch behandeln. Aus diesem Grund ist die Eigenschaft RunWorkerCompletedEventArgs.Cancelled vorhanden. Grundsätzlich erfordert dies, dass Sie Ihre Cancel-Methode als eigenständige asynchrone Methode betrachten.

Hier ist ein Beispiel:

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

Wenn Sie wirklich nicht möchten, dass Ihre Methode beendet wird, würde ich vorschlagen, ein Flag wie ein AutoResetEventauf ein abgeleitetes zu setzen BackgroundWorkerund dann zu überschreiben OnRunWorkerCompleted, um das Flag zu setzen. Es ist immer noch etwas klobig; Ich würde empfehlen, das Abbruchereignis wie eine asynchrone Methode zu behandeln und alles zu tun, was gerade im RunWorkerCompletedHandler ausgeführt wird.

OwenP
quelle
Das Verschieben von Code nach RunWorkerCompleted ist nicht dort, wo er hingehört, und nicht hübsch.
Ian Boyd
0

Ich bin ein wenig zu spät zur Party hier (ungefähr 4 Jahre), aber was ist mit dem Einrichten eines asynchronen Threads, der eine Besetztschleife verarbeiten kann, ohne die Benutzeroberfläche zu sperren? Dann muss der Rückruf von diesem Thread die Bestätigung sein, dass der BackgroundWorker den Abbruch beendet hat ?

Etwas wie das:

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

Im Wesentlichen wird dadurch ein anderer Thread ausgelöst, der im Hintergrund ausgeführt wird und nur in der Besetztschleife wartet, um festzustellen, ob der MyWorkerVorgang abgeschlossen ist. Sobald MyWorkerder Abbruch abgeschlossen ist, wird der Thread beendet und wir können damit AsyncCallbackjede Methode ausführen, die wir benötigen, um den erfolgreichen Abbruch zu verfolgen - es funktioniert wie ein Pseudo-Ereignis. Da dies vom UI-Thread getrennt ist, wird die UI nicht gesperrt, während wir darauf warten, dass MyWorkerder Abbruch abgeschlossen ist. Wenn Sie wirklich beabsichtigen, zu sperren und auf den Abbruch zu warten, ist dies für Sie nutzlos. Wenn Sie jedoch nur warten möchten, damit Sie einen anderen Prozess starten können, funktioniert dies einwandfrei.

Infotekka
quelle
0

Ich weiß, dass dies sehr spät ist (5 Jahre), aber Sie suchen nach einem Thread und einem SynchronizationContext . Sie müssen UI-Aufrufe "von Hand" an den UI-Thread zurückstellen, anstatt das Framework dies automatisch tun zu lassen.

Auf diese Weise können Sie einen Thread verwenden, auf den Sie bei Bedarf warten können.

Tom Padilla
quelle
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
Nitesh
quelle
Was ist deine Frage? In SO müssen Sie genau sein, wenn Sie Fragen stellen
Alma Do
@Dies ist Antwort keine Frage.
Code L ღ ver
0

Fredrik Kalseths Lösung für dieses Problem ist die beste, die ich bisher gefunden habe. Andere Lösungen Application.DoEvent()können Probleme verursachen oder funktionieren einfach nicht. Lassen Sie mich seine Lösung in eine wiederverwendbare Klasse umwandeln. Da BackgroundWorkeres nicht versiegelt ist, können wir unsere Klasse daraus ableiten:

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

Mit Flags und korrekter Verriegelung stellen wir sicher, dass das _resetEvent.WaitOne()wirklich nur aufgerufen wird, wenn einige Arbeiten gestartet wurden, sonst _resetEvent.Set();möglicherweise nie aufgerufen werden!

Das try-finally stellt sicher, dass _resetEvent.Set();es aufgerufen wird, auch wenn in unserem DoWork-Handler eine Ausnahme auftreten sollte. Andernfalls könnte die Anwendung beim Aufrufen für immer einfrieren CancelSync!

Wir würden es so verwenden:

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() + "%";
}

Sie können dem RunWorkerCompletedEreignis auch einen Handler hinzufügen, wie hier gezeigt:
     BackgroundWorker Class (Microsoft-Dokumentation) .

Olivier Jacot-Descombes
quelle
0

Durch das Schließen des Formulars wird meine geöffnete Protokolldatei geschlossen. Mein Hintergrundarbeiter schreibt diese Protokolldatei, sodass ich sie erst MainWin_FormClosing()beenden kann, wenn mein Hintergrundarbeiter beendet ist. Wenn ich nicht auf die Beendigung meines Hintergrundarbeiters warte, treten Ausnahmen auf.

Warum ist das so schwer?

Ein einfaches Thread.Sleep(1500)funktioniert, verzögert jedoch das Herunterfahren (wenn es zu lang ist) oder verursacht Ausnahmen (wenn es zu kurz ist).

Verwenden Sie zum Herunterfahren direkt nach dem Beenden des Hintergrundarbeiters einfach eine Variable. Das funktioniert bei mir:

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.)
A876
quelle
0

Sie können das RunWorkerCompleted-Ereignis beenden. Selbst wenn Sie bereits einen Ereignishandler für _worker hinzugefügt haben, können Sie einen weiteren hinzufügen, der in der Reihenfolge ausgeführt wird, in der er hinzugefügt wurde.

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

    ...

    public void CancelDoingStuff()
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => 
        {
            // do whatever you want to do when the cancel completes in here!
        });
        _worker.CancelAsync();
    }
}

Dies kann nützlich sein, wenn Sie mehrere Gründe haben, warum ein Abbruch auftreten kann, wodurch die Logik eines einzelnen RunWorkerCompleted-Handlers komplizierter wird als gewünscht. Abbrechen, wenn ein Benutzer versucht, das Formular zu schließen:

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (_worker != null)
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender, e) => this.Close());
        _worker.CancelAsync();
        e.Cancel = true;
    }
}
Shawn Rubie
quelle
0

Ich benutze die asyncMethode und awaitwarte, bis der Arbeiter seine Arbeit beendet hat:

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

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

und in DoWorkMethode:

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

Sie können auch die kapseln whileSchleife in DoWorkmit try ... catchzu Satz _isBusyist falseauf Ausnahme. Oder checken Sie einfach _worker.IsBusydie StopAsyncwhile-Schleife ein.

Hier ist ein Beispiel für die vollständige Implementierung:

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

So stoppen Sie den Arbeiter und warten, bis er bis zum Ende läuft:

await myBackgroundWorker.StopAsync();

Die Probleme mit dieser Methode sind:

  1. Sie müssen den ganzen Weg asynchrone Methoden verwenden.
  2. Warten Sie auf Task.Delay ist ungenau. Auf meinem PC wartet Task.Delay (1) tatsächlich ~ 20 ms.
Anthony
quelle
-2

Oh Mann, einige davon sind lächerlich komplex geworden. Sie müssen lediglich die Eigenschaft BackgroundWorker.CancellationPending im DoWork-Handler überprüfen. Sie können es jederzeit überprüfen. Sobald es aussteht, setzen Sie e.Cancel = True und beenden Sie die Methode.

// Methode hier private void Worker_DoWork (Objektabsender, DoWorkEventArgs e) {BackgroundWorker bw = (Absender als BackgroundWorker);

// do stuff

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

// do other stuff

}}


quelle
1
Und wie muss die Person, die CancelAsync angerufen hat, mit dieser Lösung warten, bis der Hintergrundarbeiter abbricht?
Ian Boyd