it-swarm.com.de

Wie kann ich BackgroundWorker beim Formularabschluss beenden?

Ich habe ein Formular, das einen BackgroundWorker erzeugt, der das eigene Textfeld des Formulars (im Haupt-Thread) aktualisieren sollte, daher Invoke((Action) (...));-Aufruf.
Wenn in HandleClosingEvent ich nur bgWorker.CancelAsync() mache, dann bekomme ich ObjectDisposedException bei Invoke(...) angerufen, verständlicherweise. Aber wenn ich in HandleClosingEvent sitze und warte, bis bgWorker fertig ist, kehrt .Invoke (...) niemals zurück, auch verständlich. 

Wie kann ich diese App schließen, ohne die Ausnahme oder den Deadlock zu erhalten?

Es folgen 3 relevante Methoden der einfachen Klasse Form1:

    public Form1() {
        InitializeComponent();
        Closing += HandleClosingEvent;
        this.bgWorker.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
        while (!this.bgWorker.CancellationPending) {
            Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToString(); }));
        }
    }

    private void HandleClosingEvent(object sender, CancelEventArgs e) {
        this.bgWorker.CancelAsync();
        /////// while (this.bgWorker.CancellationPending) {} // deadlock
    }
65
THX-1138

Die einzige Deadlock-sichere und ausnahmesichere Möglichkeit, dies zu tun, ist das Abbrechen des FormClosing-Ereignisses. Setzen Sie e.Cancel = true, wenn die BGW noch ausgeführt wird, und setzen Sie ein Flag, um anzuzeigen, dass der Benutzer eine Schließung angefordert hat. Überprüfen Sie dann dieses Flag in der Ereignisbehandlungsroutine RunWorkerCompleted der BGW, und rufen Sie Close () auf, wenn es festgelegt ist.

private bool closePending;

protected override void OnFormClosing(FormClosingEventArgs e) {
    if (backgroundWorker1.IsBusy) {
        closePending = true;
        backgroundWorker1.CancelAsync();
        e.Cancel = true;
        this.Enabled = false;   // or this.Hide()
        return;
    }
    base.OnFormClosing(e);
}

void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if (closePending) this.Close();
    closePending = false;
    // etc...
}
94
Hans Passant

Ich habe einen anderen Weg gefunden. Wenn Sie mehr Hintergrundarbeiter haben, können Sie Folgendes erstellen:

List<Thread> bgWorkersThreads  = new List<Thread>();

und in jedem backgroundWorker's DoWork-Verfahren:

bgWorkesThreads.Add(Thread.CurrentThread);

Arter, den Sie verwenden können:

foreach (Thread thread in this.bgWorkersThreads) 
{
     thread.Abort();    
}

Ich habe dies in Word Add-In in Control verwendet, das ich in CustomTaskPane verwende. Wenn jemand das Dokument oder die Anwendung früher schließt, dann beenden alle meine backgroundWorkes ihre Arbeit. Dies führt zu einem COM Exception (ich weiß nicht genau, welcher) .CancelAsync() nicht funktioniert.

Aber damit kann ich alle Threads schließen, die von backgroundworkers Sofort in DocumentBeforeClose verwendet werden, und mein Problem ist gelöst.

3
Bronix

Hier war meine Lösung (Sorry, es ist in VB.Net). 

Beim Ausführen des FormClosing-Ereignisses führe ich BackgroundWorker1.CancelAsync () aus, um den CancellationPending-Wert auf True zu setzen. Leider hat das Programm nie wirklich die Möglichkeit, den Wert von CancellationPending zu prüfen, um e.Cancel auf true zu setzen (was meines Erachtens nur in BackgroundWorker1_DoWork möglich ist). Ich habe diese Zeile nicht entfernt. obwohl es nicht wirklich einen Unterschied zu machen scheint.

Ich fügte eine Zeile hinzu, die meine globale Variable, bClosingForm, auf True setzt. Dann fügte ich meinem BackgroundWorker_WorkCompleted eine Codezeile hinzu, um sowohl e.Cancelled als auch die globale Variable bClosingForm zu überprüfen, bevor ich die Endschritte ausführte. 

Wenn Sie diese Vorlage verwenden, sollten Sie Ihr Formular jederzeit schließen können, auch wenn sich der Hintergrundarbeiter gerade in einer Situation befindet (was möglicherweise nicht gut ist, aber es wird zwangsläufig vorkommen, damit es auch behandelt werden kann). Ich bin nicht sicher, ob es notwendig ist, aber Sie könnten den Background-Worker vollständig im Form_Closed-Ereignis entsorgen, nachdem dies alles erfolgt ist.

Private bClosingForm As Boolean = False

Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    bClosingForm = True
    BackgroundWorker1.CancelAsync() 
End Sub

Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    'Run background tasks:
    If BackgroundWorker1.CancellationPending Then
        e.Cancel = True
    Else
        'Background work here
    End If
End Sub

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
    If Not bClosingForm Then
        If Not e.Cancelled Then
            'Completion Work here
        End If
    End If
End Sub
2
Tim Hagel

Können Sie nicht auf das Signal im Destruktor des Formulars warten? 

AutoResetEvent workerDone = new AutoResetEvent();

private void HandleClosingEvent(object sender, CancelEventArgs e)
{
    this.bgWorker.CancelAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (!this.bgWorker.CancellationPending) {
        Invoke((Action) (() => { this.textBox1.Text =   
                                 Environment.TickCount.ToString(); }));
    }
}


private ~Form1()
{
    workerDone.WaitOne();
}


void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
{
    workerDone.Set();
}
1
Cheeso

Erstens ist die ObjectDisposedException hier nur eine mögliche Gefahr. Das Ausführen des OP-Codes hat bei einer beträchtlichen Anzahl von Gelegenheiten die folgende InvalidOperationException erzeugt:

Invoke oder BeginInvoke kann nicht aufgerufen werden auf einem Steuerelement bis zum Fenstergriff wurde erschaffen.

Ich nehme an, dass dies geändert werden könnte, indem der Worker mit dem 'Loaded'-Callback und nicht mit dem Konstruktor gestartet wird. Diese ganze Prüfung kann jedoch ganz vermieden werden, wenn der Fortschrittsmeldemechanismus von BackgroundWorker verwendet wird. Folgendes funktioniert gut: 

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (!this.bgWorker.CancellationPending)
    {
        this.bgWorker.ReportProgress(Environment.TickCount);
        Thread.Sleep(1);
    }
}

private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.textBox1.Text = e.ProgressPercentage.ToString();
}

Ich habe den prozentualen Parameter irgendwie gekapert, aber man kann die andere Überladung verwenden, um einen beliebigen Parameter zu übergeben. 

Es ist interessant festzustellen, dass das Entfernen des obigen Sleep-Aufrufs die Benutzeroberfläche verstopft, eine hohe CPU-Belastung erfordert und die Speicherauslastung kontinuierlich erhöht. Ich denke, es hat etwas mit der Message Queue der GUI zu tun, die überladen wird. Bei intaktem Sleep-Aufruf ist die CPU-Auslastung jedoch praktisch 0 und die Speicherauslastung scheint ebenfalls in Ordnung zu sein. Um vorsichtig zu sein, sollte vielleicht ein höherer Wert als 1 ms verwendet werden? Ein Gutachten hier wäre wünschenswert ... Update: Es scheint so, als ob das Update nicht zu häufig ist, sollte es in Ordnung sein: Link

Ich kann auf keinen Fall ein Szenario vorhersehen, in dem die Aktualisierung der GUI in Intervallen von weniger als einigen Millisekunden erfolgen muss (zumindest in Szenarien, in denen ein Mensch die GUI beobachtet), also denke ich die meiste Zeit Fortschrittsberichte wären die richtige Wahl

1
Ohad Schneider

Ihr Hintergrundarbeiter sollte Invoke nicht verwenden, um das Textfeld zu aktualisieren. Es sollte den UI-Thread auffordern, das Textfeld mithilfe des Ereignisses ProgressChanged mit dem Wert in das angehängte Textfeld zu aktualisieren.

Während des Ereignisses Closed (oder möglicherweise des Ereignisses Closing) erinnert sich der UI-Thread daran, dass das Formular geschlossen ist, bevor der Hintergrundarbeiter abgebrochen wird.

Beim Empfang von progressChanged prüft der UI-Thread, ob das Formular geschlossen ist, und aktualisiert das Textfeld nur, wenn dies nicht der Fall ist.

0

Ich verstehe wirklich nicht, warum DoEvents in diesem Fall als eine so schlechte Wahl angesehen wird, wenn Sie this.enabled = false verwenden. Ich denke, das würde ganz ordentlich werden.

protected override void OnFormClosing(FormClosingEventArgs e) {

    this.Enabled = false;   // or this.Hide()
    e.Cancel = true;
    backgroundWorker1.CancelAsync();  

    while (backgroundWorker1.IsBusy) {

        Application.DoEvents();

    }

    e.cancel = false;
    base.OnFormClosing(e);

}
0

Dies funktioniert nicht für alle, aber wenn Sie in einem BackgroundWorker regelmäßig etwas tun, wie jede Sekunde oder alle 10 Sekunden (möglicherweise ein Server abfragen), scheint dies gut zu funktionieren, um den Prozess ordnungsgemäß und ohne Fehlermeldung zu stoppen (zumindest soweit) und ist leicht zu folgen;

 public void StopPoll()
        {
            MyBackgroundWorker.CancelAsync(); //Cancel background worker
            AutoResetEvent1.Set(); //Release delay so cancellation occurs soon
        }

 private void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            while (!MyBackgroundWorker.CancellationPending)
            {
            //Do some background stuff
            MyBackgroundWorker.ReportProgress(0, (object)SomeData);
            AutoResetEvent1.WaitOne(10000);
            }
    }
0
SnowMan50