Jak czekać na anulowanie BackgroundWorkera?

125

Rozważ hipotetyczną metodę obiektu, który działa za Ciebie:

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

    ...

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

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

Jak można czekać, aż BackgroundWorker zostanie ukończony?


W przeszłości ludzie próbowali:

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

Ale to zakleszczenie , ponieważ IsBusynie jest czyszczone, dopóki nie RunWorkerCompletedzostanie obsłużone zdarzenie, a to zdarzenie nie może zostać obsłużone, dopóki aplikacja nie przejdzie w stan bezczynności. Aplikacja nie będzie bezczynna, dopóki pracownik nie skończy. (Poza tym jest to zajęta pętla - obrzydliwa.)

Inni dodali sugestię umieszczenia go w:

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

Problem polega na tym, że Application.DoEvents()powoduje przetwarzanie komunikatów znajdujących się w kolejce, co powoduje problemy z ponownym wejściem (.NET nie jest ponownie wprowadzany).

Chciałbym użyć rozwiązania obejmującego obiekty synchronizacji zdarzeń, w których kod czeka na zdarzenie - ustawiane przez RunWorkerCompletedprogramy obsługi zdarzeń pracownika . Coś jak:

Event _workerDoneEvent = new WaitHandle();

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

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

Ale wracam do impasu: program obsługi zdarzeń nie może działać, dopóki aplikacja nie przejdzie w stan bezczynności, a aplikacja nie przejdzie w stan bezczynności, ponieważ czeka na zdarzenie.

Jak więc możesz czekać na zakończenie pracy w tle?


Aktualizacja Ludzie wydają się być zdezorientowani tym pytaniem. Wydaje się, że myślą, że będę używać BackgroundWorker jako:

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

To jest nie to, że to nie to, co robię, a to nie to, co jest proszony jest tutaj. Gdyby tak było, nie byłoby sensu korzystać z pracownika w tle.

Ian Boyd
źródło

Odpowiedzi:

130

Jeśli dobrze zrozumiem twoje wymagania, możesz zrobić coś takiego (kod nie jest testowany, ale pokazuje ogólną ideę):

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
źródło
7
Spowoduje to zablokowanie interfejsu użytkownika (np. Brak ponownego malowania), jeśli anulowanie pracy pracownika w tle zajmuje dużo czasu. Lepiej jest użyć jednego z poniższych, aby zatrzymać interakcję z interfejsem użytkownika: stackoverflow.com/questions/123661/…
Joe,
1
+1 dokładnie to, co zlecił lekarz ... chociaż zgadzam się z @Joe, jeśli prośba o anulowanie może zająć więcej niż sekundę.
dotjoe
Co się stanie, gdy CancelAsync zostanie obsłużona przed WaitOne? A może przycisk Anuluj działa tylko raz.
CodingBarfield
6
Musiałem sprawdzić, ((BackgroundWorker)sender).CancellationPendingaby uzyskać odwołanie wydarzenia
Luuk
1
Jak wspomniał Luuk, to nie właściwość Cancel musi być zaznaczona, ale CancellationPending. Po drugie, jak wspomina Joel Coehoorn, oczekiwanie na zakończenie wątku uniemożliwia użycie nici w pierwszej kolejności.
Kapitan Sensible
15

Wystąpił problem z odpowiedzią. Interfejs użytkownika musi nadal przetwarzać wiadomości podczas oczekiwania, w przeciwnym razie nie zostanie ponownie malowany, co będzie problemem, jeśli pracownik działający w tle potrzebuje dużo czasu, aby odpowiedzieć na żądanie anulowania.

Druga wada polega na tym, _resetEvent.Set()że nigdy nie zostanie wywołana, jeśli wątek roboczy zgłosi wyjątek - pozostawiając główny wątek w oczekiwaniu na czas nieokreślony - jednak tę wadę można łatwo naprawić za pomocą bloku try / final.

Jednym ze sposobów jest wyświetlenie modalnego okna dialogowego z licznikiem czasu, który wielokrotnie sprawdza, czy pracownik w tle zakończył pracę (lub zakończył anulowanie w Twoim przypadku). Po zakończeniu pracy w tle modalne okno dialogowe przywraca sterowanie do aplikacji. Użytkownik nie może wchodzić w interakcje z interfejsem użytkownika, dopóki to nie nastąpi.

Inną metodą (zakładając, że masz otwarte maksymalnie jedno niemodalne okno) jest ustawienie ActiveForm.Enabled = false, a następnie pętla na Application, DoEvents, aż proces roboczy w tle zakończy anulowanie, po czym możesz ponownie ustawić ActiveForm.Enabled = true.

Joe
źródło
5
Może to stanowić problem, ale jest to zasadniczo akceptowane jako część pytania „Jak czekać na anulowanie BackgroundWorkera”. Czekanie oznacza czekanie, nic więcej nie robisz. Obejmuje to również przetwarzanie wiadomości. Jeśli nie chciałbym czekać na pracownika w tle, możesz po prostu zadzwonić do .CancelAsync. Ale to nie jest tutaj wymóg projektowy.
Ian Boyd,
1
+1 za wskazanie wartości metody CancelAsync, wersety czekające na proces roboczy w tle.
Ian Boyd,
10

Prawie każdy z was jest zdezorientowany tym pytaniem i nie rozumie, w jaki sposób wykorzystuje się pracownika.

Rozważ obsługę zdarzeń 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;
}

I wszystko dobrze.

Teraz pojawia się sytuacja, w której dzwoniący musi przerwać odliczanie, ponieważ musi wykonać awaryjne samozniszczenie rakiety.

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

    StartClaxon();
    SelfDestruct();
}

Jest też sytuacja, w której musimy otworzyć bramki dostępu do rakiety, ale nie podczas odliczania:

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

Na koniec musimy zmniejszyć paliwo rakiety, ale nie jest to dozwolone podczas odliczania:

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

    if (rocketOnPad)
        OpenFuelValves();
}

Bez możliwości oczekiwania na anulowanie przez pracownika, musimy przenieść wszystkie trzy metody do 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();
}

Teraz mógłbym napisać swój kod w ten sposób, ale po prostu nie zamierzam. Nie obchodzi mnie to, po prostu nie.

Ian Boyd
źródło
18
Gdzie jest metoda WaitForWorkerToFinish? jakikolwiek pełny kod źródłowy?
Kiquenet,
4

Możesz sprawdzić RunWorkerCompletedEventArgs w RunWorkerCompletedEventHandler, aby zobaczyć, jaki był stan. Powodzenie, anulowanie lub błąd.

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

Aktualizacja : Aby sprawdzić, czy pracownik wywołał .CancelAsync (), używając tego:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
Seb Nilsson
źródło
2
Wymagane jest, aby CancelDoingStuff () nie mogło powrócić do czasu zakończenia procesu roboczego. Sprawdzanie, jak to się udało, naprawdę nie jest interesujące.
Ian Boyd
Następnie będziesz musiał utworzyć wydarzenie. To naprawdę nie ma nic wspólnego z BackgroundWorkerem, wystarczy zaimplementować zdarzenie, posłuchać go i uruchomić po zakończeniu. A RunWorkerCompletedEventHandler jest gotowe. Odpal kolejne wydarzenie.
Seb Nilsson,
4

Państwo nie czekać na pracownika tle, aby zakończyć. To w dużej mierze przeczy celowi uruchomienia osobnego wątku. Zamiast tego powinieneś pozwolić swojej metodzie zakończyć i przenieść kod, który zależy od zakończenia, w inne miejsce. Pozwalasz pracownikowi powiedzieć ci, kiedy to się skończy, i wywołać pozostały kod.

Jeśli chcesz poczekać, aż coś się zakończy, użyj innej konstrukcji wątkowej, która udostępnia WaitHandle.

Joel Coehoorn
źródło
1
Czy możesz coś zasugerować? Wydaje się, że proces roboczy w tle jest jedyną konstrukcją wątkową, która może wysyłać powiadomienia do wątku, który utworzył obiekt BackgroundWorker.
Ian Boyd
Backgroundworker jest konstrukcją interfejsu użytkownika . Po prostu wykorzystuje zdarzenia, które Twój własny kod nadal musi wiedzieć, jak wywołać. Nic nie stoi na przeszkodzie, aby stworzyć w tym celu swoich własnych delegatów.
Joel Coehoorn
3

Dlaczego nie możesz po prostu połączyć się ze zdarzeniem BackgroundWorker.RunWorkerCompleted. Jest to wywołanie zwrotne, które „Wystąpi, gdy operacja w tle zostanie zakończona, anulowana lub zgłosi wyjątek”.

Rick Minerich
źródło
1
Ponieważ osoba, która używa obiektu DoesStuff, poprosiła go o anulowanie. Udostępnione zasoby, z których korzysta obiekt, zostaną wkrótce odłączone, usunięte, zutylizowane, zamknięte i muszą wiedzieć, że zostało to zrobione, aby móc kontynuować.
Ian Boyd,
1

Nie rozumiem, dlaczego chcesz czekać na zakończenie pracy w tle; naprawdę wydaje się być dokładnym przeciwieństwem motywacji dla klasy.

Jednak każdą metodę można rozpocząć od wywołania metody worker.IsBusy i nakazać jej zakończenie, jeśli jest uruchomiona.

Austin Salonen
źródło
Zły dobór słów; nie czekam, aż to się zakończy.
Ian Boyd,
1

Chcę tylko powiedzieć, że przyszedłem tutaj, ponieważ potrzebuję pracownika działającego w tle, aby poczekał, aż uruchomię proces asynchroniczny w pętli, moja poprawka była znacznie łatwiejsza niż wszystkie inne rzeczy ^^

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

Pomyślałem, że się tym podzielę, ponieważ właśnie w tym miejscu znalazłem rozwiązanie. Jest to również mój pierwszy post na temat przepełnienia stosu, więc jeśli jest zły lub cokolwiek, chciałbym krytyków! :)

Connor Williams
źródło
0

Hm, może nie rozumiem dobrze twojego pytania.

Pracownik w tle wywołuje zdarzenie WorkerCompleted po zakończeniu jego „workermethod” (metoda / funkcja / podrzędna obsługująca zdarzenie backgroundworker.doWork ), więc nie ma potrzeby sprawdzania, czy BW nadal działa. Jeśli chcesz zatrzymać pracownika, sprawdź oczekującą nieruchomość w ramach swojej „metody pracowniczej”.

Stephan
źródło
Rozumiem, że pracownik musi monitorować właściwość CancellationPending i wyjść tak szybko, jak to możliwe. Ale w jaki sposób osoba z zewnątrz, która poprosiła pracownika w tle o anulowanie, czeka, aż zostanie to zrobione?
Ian Boyd,
Zdarzenie WorkCompleted będzie nadal uruchamiane.
Joel Coehoorn,
„Ale w jaki sposób osoba z zewnątrz, która poprosiła pracownika w tle, anuluje, zaczeka, aż zostanie to zrobione?”
Ian Boyd,
Czekasz, aż zostanie to wykonane, czekając na uruchomienie zdarzenia WorkCompleted. Jeśli chcesz uniemożliwić użytkownikowi dopasowanie kliknięcia do całego GUI, możesz użyć jednego z rozwiązań zaproponowanych przez @Joe w jego odpowiedzi powyżej (wyłącz formularz lub pokaż coś modalnego). Innymi słowy, pozwól pętli bezczynności systemu czekać na Ciebie; powie ci, kiedy się skończy (odpalając zdarzenie).
Geoff
0

Przepływ pracy BackgroundWorkerobiektu zasadniczo wymaga obsługi RunWorkerCompletedzdarzenia zarówno w przypadku normalnego wykonywania, jak i przypadków użycia anulowania przez użytkownika. Dlatego istnieje właściwość RunWorkerCompletedEventArgs.Cancelled . Zasadniczo wykonanie tego poprawnie wymaga rozważenia metody Cancel jako metody asynchronicznej.

Oto przykład:

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

Jeśli naprawdę nie chcesz, aby twoja metoda zakończyła się, sugerowałbym wstawienie flagi takiej jak AutoResetEventna pochodnym BackgroundWorker, a następnie przesłonięcie, OnRunWorkerCompletedaby ustawić flagę. Jednak wciąż jest trochę niezdarnie; Zalecam traktowanie zdarzenia Cancel jako metody asynchronicznej i robienie wszystkiego, co aktualnie robi w RunWorkerCompletedmodule obsługi.

OwenP
źródło
Przeniesienie kodu do RunWorkerCompleted nie jest tam, gdzie należy i nie jest ładne.
Ian Boyd,
0

Jestem trochę spóźniony na imprezę tutaj (około 4 lata), ale co z konfiguracją wątku asynchronicznego, który może obsłużyć zajętą ​​pętlę bez blokowania interfejsu użytkownika, a następnie wywołanie zwrotne z tego wątku będzie potwierdzeniem, że BackgroundWorker zakończył anulowanie ?

Coś takiego:

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

W istocie to, co robi, to odpalanie innego wątku, aby działał w tle, który po prostu czeka w swojej zajętej pętli, aby sprawdzić, czy MyWorkerzakończyło się. Po MyWorkerzakończeniu anulowania wątek zostanie zakończony i możemy go użyć AsyncCallbackdo wykonania dowolnej metody, której potrzebujemy, aby po pomyślnym anulowaniu - będzie działać jak pseudo-zdarzenie. Ponieważ jest to niezależne od wątku interfejsu użytkownika, nie blokuje interfejsu użytkownika, gdy czekamy na MyWorkerzakończenie anulowania. Jeśli naprawdę chcesz zablokować i czekać na anulowanie, jest to dla ciebie bezużyteczne, ale jeśli chcesz tylko poczekać, aby móc rozpocząć kolejny proces, to działa dobrze.

Infotekka
źródło
0

Wiem, że jest to naprawdę późno (5 lat), ale to, czego szukasz, to użyć wątku i SynchronizationContext . Będziesz musiał kierować wywołania interfejsu użytkownika z powrotem do wątku interfejsu użytkownika „ręcznie”, zamiast pozwolić, aby Framework robił to automatycznie.

Pozwala to na użycie wątku, na który możesz poczekać, jeśli zajdzie taka potrzeba.

Tom Padilla
źródło
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
źródło
Jakie jest Twoje pytanie? W SO musisz być konkretny, zadając pytania
Alma Do
@ To jest odpowiedź, a nie pytanie.
Kod L ღ wer.
0

Rozwiązanie Fredrika Kalsetha na ten problem jest najlepsze, jakie do tej pory znalazłem. Inne rozwiązania, Application.DoEvent()które mogą powodować problemy lub po prostu nie działają. Pozwólcie, że wrzucę jego rozwiązanie do klasy wielokrotnego użytku. Ponieważ BackgroundWorkernie jest zapieczętowany, możemy wyprowadzić z niego naszą klasę:

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

Dzięki flagom i odpowiedniemu blokowaniu upewniamy się, że _resetEvent.WaitOne()naprawdę zostanie wywołany tylko wtedy, gdy jakaś praca została rozpoczęta, w przeciwnym razie _resetEvent.Set();może nigdy nie zostać wywołana!

Próba ostatecznie zapewnia, że _resetEvent.Set();zostanie wywołana, nawet jeśli w naszej obsłudze DoWork powinien wystąpić wyjątek. W przeciwnym razie aplikacja może zawiesić się na zawsze podczas wywoływania CancelSync!

Używalibyśmy tego w ten sposób:

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

Możesz również dodać procedurę obsługi do RunWorkerCompletedzdarzenia, jak pokazano tutaj:
     BackgroundWorker Class (dokumentacja Microsoft) .

Olivier Jacot-Descombes
źródło
0

Zamknięcie formularza powoduje zamknięcie mojego otwartego pliku dziennika. Pracownik działający w tle zapisuje ten plik dziennika, więc nie mogę MainWin_FormClosing()zakończyć, dopóki nie zakończy się proces roboczy w tle. Jeśli nie czekam na zakończenie pracy mojego pracownika działającego w tle, zdarzają się wyjątki.

Dlaczego to takie trudne?

Prosty Thread.Sleep(1500)działa, ale opóźnia zamknięcie (jeśli jest zbyt długi) lub powoduje wyjątki (jeśli jest zbyt krótki).

Aby zamknąć zaraz po zakończeniu procesu roboczego w tle, wystarczy użyć zmiennej. To działa dla mnie:

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
źródło
0

Możesz wycofać się ze zdarzenia RunWorkerCompleted. Nawet jeśli dodałeś już moduł obsługi zdarzeń dla _worker, możesz dodać kolejny i będą one wykonywane w kolejności, w jakiej zostały dodane.

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

może to być przydatne, jeśli masz wiele powodów, dla których może wystąpić anulowanie, co sprawia, że ​​logika pojedynczego programu obsługi RunWorkerCompleted jest bardziej skomplikowana niż chcesz. Na przykład anulowanie, gdy użytkownik próbuje zamknąć formularz:

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
źródło
0

Stosuję asyncmetodę i awaitczekam, aż pracownik zakończy pracę:

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

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

i w DoWorkmetodzie:

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

Można też hermetyzacji whilepętlę w DoWorkz try ... catchna zestaw _isBusyjest falsena wyjątku. Lub po prostu sprawdź _worker.IsBusyw StopAsyncpętli while.

Oto przykład pełnej realizacji:

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

Aby zatrzymać pracownika i poczekać, aż dobiegnie do końca:

await myBackgroundWorker.StopAsync();

Problemy z tą metodą to:

  1. Musisz używać metod asynchronicznych przez całą drogę.
  2. await Zadanie.Delay jest niedokładne. Na moim komputerze Task.Delay (1) faktycznie czeka ~ 20 ms.
Anthony
źródło
-2

o rany, niektóre z nich stały się absurdalnie złożone. wszystko, co musisz zrobić, to sprawdzić właściwość BackgroundWorker.CancellationPending wewnątrz procedury obsługi DoWork. w każdej chwili możesz to sprawdzić. gdy jest w toku, ustaw e.Cancel = True i zwolnij z metody.

// metoda tutaj private void Worker_DoWork (nadawca obiektu, DoWorkEventArgs e) {BackgroundWorker bw = (nadawca jako BackgroundWorker);

// do stuff

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

// do other stuff

}


źródło
1
W przypadku tego rozwiązania, w jaki sposób osoba, która zadzwoniła do CancelAsync, jest zmuszona czekać, aż pracownik w tle anuluje?
Ian Boyd,