Comment attendre l'annulation d'un BackgroundWorker?

Considérer un hypothétique méthode d'un objet qui n'choses pour vous:

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

    ...

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

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

comment peut-on attendre qu'un travailleur de fond soit fait?


dans le passé les gens ont essayé:

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

mais ces blocages , parce que IsBusy n'est pas effacé jusqu'à ce que l'événement RunWorkerCompleted est manipulé, et cet événement ne peut pas obtenir jusqu'à ce que l'application tourne au ralenti. L'application ne restera pas inactive jusqu'à ce que le travailleur ait terminé. (En Plus, c'est une boucle occupée - dégoûtant.)

D'autres ont ajouté la suggestion suivante:

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

le problème avec cela est que Application.DoEvents() fait que les messages actuellement dans la file d'attente sont traités, ce qui cause des problèmes de rentrée (.NET n'est pas re-entrant).

j'espère utiliser une solution impliquant un événement objets de synchronisation, où le code attend pour un événement-que le travailleur RunWorkerCompleted event handlers fixe. Quelque chose comme:

Event _workerDoneEvent = new WaitHandle();

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

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

mais je suis de retour dans l'impasse: le gestionnaire d'événements ne peut pas fonctionner tant que l'application ne tourne pas au ralenti, et l'application ne va pas tourner au ralenti parce qu'elle attend un événement.

alors comment peut-on attendre qu'un travailleur de fond termine?


mise à Jour Les gens semblent confus par cette question. Ils semblent penser que je vais utiliser le travailleur de fond comme:

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

Qui est pas , c'est pas ce que je fais, et c'est pas ce qui est demandé ici. Si tel était le cas, il ne servirait à rien d'avoir recours à un travailleur d'arrière-plan.

114
demandé sur A876 2008-09-24 00:30:50

17 réponses

si je comprends bien votre exigence, vous pouvez faire quelque chose comme ceci (code non testé, mais montre l'idée générale):

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
}
121
répondu Fredrik Kalseth 2008-09-24 07:59:30

il y a un problème avec cette réponse . L'UI doit continuer à traiter les messages pendant que vous attendez, sinon il ne repeindra pas, ce qui sera un problème si votre travailleur d'arrière-plan prend un long temps pour répondre à la demande d'annulation.

un second défaut est que _resetEvent.Set() ne sera jamais appelé si le worker thread lance une exception-laissant le fil principal attendre indéfiniment - cependant ce défaut pourrait facilement être corrigé avec un try/finally bloc.

une façon de faire ceci est d'afficher un Dialogue modal qui a un timer qui vérifie à plusieurs reprises si le travailleur d'arrière-plan a fini le travail (ou fini l'annulation dans votre cas). Une fois le travail de fond terminé, le Dialogue modal renvoie le contrôle à votre application. L'utilisateur ne peut pas interagir avec l'INTERFACE utilisateur jusqu'à ce que cela arrive.

une autre méthode (en supposant que vous avez un maximum d'une fenêtre modeless ouverte) est de définir ActiveForm.Enabled = false, puis loop on Application, DoEvents jusqu'à ce que le travailleur de fond ait terminé l'annulation, après quoi vous pouvez définir ActiveForm.Enabled = true à nouveau.

13
répondu Joe 2017-05-23 11:54:59

la plupart d'entre vous sont confus par la question, et ne comprennent pas comment un travailleur est utilisé.

Considérer un RunWorkerComplete gestionnaire d'événement:

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

Et tout est bon.

arrive maintenant une situation où l'appelant doit annuler le compte à rebours parce qu'il doit exécuter une autodestruction d'urgence de la fusée.

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

    StartClaxon();
    SelfDestruct();
}

et il y a aussi une situation où nous devons ouvrez les portes d'accès à la fusée, mais pas en faisant un compte à rebours:

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

    if (!rocketOnPad)
        DisengageAllGateLatches();
}

et enfin, nous avons besoin de désalimenter la fusée, mais ce n'est pas autorisé lors d'un compte à rebours:

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

    if (rocketOnPad)
        OpenFuelValves();
}

sans la possibilité d'attendre qu'un travailleur annule, nous devons déplacer les trois méthodes à la 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();
}

maintenant je peux écrire mon code comme ça, mais je ne vais pas le faire. Je ne m'inquiète pas, je ne suis pas.

10
répondu Ian Boyd 2017-04-28 18:59:27

vous pouvez vérifier dans le RunWorkerCompletedEventArgs dans le RunWorkerCompletedEventHandler pour voir quel était le statut. Succès, annulé ou une erreur.

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

mise à Jour : Pour voir si votre travailleur a appelé .CancelAsync () en utilisant ceci:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}
4
répondu Seb Nilsson 2008-09-23 20:41:57

Vous ne pas attendre pour le fond travailleur à remplir. Cela va à peu près à l'encontre de l'objectif de lancer un thread séparé. Au lieu de cela, vous devriez laisser votre méthode finir, et déplacer n'importe quel code qui dépend de l'achèvement à un endroit différent. Vous laissez le travailleur vous dire quand c'est fait et appelez n'importe quel code restant alors.

si vous voulez attendez pour quelque chose à compléter utilisez une construction de fil différente qui fournit un WaitHandle.

4
répondu Joel Coehoorn 2008-09-23 20:43:42

pourquoi tu ne peux pas juste t'attacher à L'arrière-plan du travailleur.Runworker A Terminé L'Événement. C'est un rappel qui "se produira lorsque l'opération de fond est terminée, a été annulée, ou a soulevé une exception."

3
répondu Rick Minerich 2008-09-23 20:39:39

je ne comprends pas pourquoi vous voulez attendre un BackgroundWorker, il semble vraiment comme l'exact opposé de la motivation pour la classe.

cependant, vous pourriez commencer chaque méthode par un appel à un travailleur.IsBusy et les faire sortir s'il est en cours d'exécution.

1
répondu Austin Salonen 2008-09-23 20:41:31

je voulais juste dire que je suis venu ici parce que j'ai besoin d'un travailleur d'arrière-plan pour attendre pendant que j'exécutais un processus asynchrone alors que dans une boucle, ma réparation était beaucoup plus facile que tous ces autres trucs^^

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

vient de penser que je partagerais parce que c'est là que j'ai fini en cherchant une solution. Aussi, c'est mon premier post sur stack overflow donc si son mauvais ou quoi que ce soit, j'aimerais critiques! :)

1
répondu Connor Williams 2017-03-08 12:00:41

HM peut-être que je ne comprends pas votre question.

le travailleur de fond appelle l'événement terminé le travailleur une fois sa "méthode de travail" (la méthode/la fonction/le sous-système qui traite le travailleur de fond .doWork-event ) est terminé de sorte qu'il n'est pas nécessaire de vérifier si le BW est toujours en cours d'exécution. Si vous voulez arrêter votre travailleur, cochez la case Annulation en instance de propriété à l'intérieur de votre "méthode du travailleur".

0
répondu Stephan 2008-09-23 20:42:53

le flux de travail d'un objet BackgroundWorker nécessite essentiellement de gérer l'événement RunWorkerCompleted pour l'exécution normale et les cas d'utilisation d'annulation par l'utilisateur. C'est pourquoi la propriété RunWorkerCompletedEventArgs.Annulé existe. Fondamentalement, faire ceci correctement exige que vous considériez votre méthode D'Annulation comme une méthode asynchrone en soi.

voici un exemple:

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

Si vous vraiment vraiment vraiment ne veulent pas que votre méthode de sortir, je suggérerais de mettre un drapeau comme un AutoResetEvent sur un dérivé BackgroundWorker , puis remplacer OnRunWorkerCompleted pour mettre le drapeau. Il est encore un peu de kludgy cependant; je recommande de traiter l'événement d'annulation comme une méthode asynchrone et de faire ce qu'il est en train de faire dans le RunWorkerCompleted handler.

0
répondu OwenP 2008-09-23 21:02:45

je suis un peu en retard à la fête ici (environ 4 ans) mais qu'en est-il de la mise en place d'un fil asynchrone qui peut gérer une boucle occupée sans verrouiller L'interface utilisateur, puis avoir le rappel de ce fil être la confirmation que le BackgroundWorker a terminé l'annulation?

quelque chose comme ça:

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

essentiellement ce que cela fait est d'allumer un autre fil pour exécuter en arrière-plan qui attend juste dans sa boucle occupée pour voir si le MyWorker est terminé. Une fois que MyWorker a terminé d'annuler le thread va sortir et nous pouvons utiliser son AsyncCallback pour exécuter n'importe quelle méthode dont nous avons besoin pour suivre l'annulation réussie - il va fonctionner comme un psuedo-événement. Puisque ceci est séparé du thread D'UI, il ne verrouillera pas L'UI pendant que nous attendons MyWorker pour finir d'annuler. Si votre intention est vraiment de verrouillage et d'attente pour les annuler ensuite c'est inutile pour vous, mais si vous voulez juste attendre de sorte que vous pouvez commencer une autre processus alors cela fonctionne bien.

0
répondu Infotekka 2012-07-06 23:08:24

je sais que c'est vraiment en retard (5 ans) mais ce que vous cherchez est d'utiliser un fil et un SynchronizationContext . Vous allez devoir marshaliser les appels UI de nouveau au fil UI" à la main " plutôt que de laisser le cadre le faire auto-magiquement.

Cela vous permet d'utiliser un Thread que vous pouvez Attendre si besoin est.

0
répondu Tom Padilla 2013-10-11 20:06:34
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
0
répondu Nitesh 2014-04-26 22:12:23

la solution de Fredrik Kalseth à ce problème est la meilleure que j'ai trouvée jusqu'à présent. D'autres solutions utilisent Application.DoEvent() qui peuvent causer des problèmes ou ne fonctionnent tout simplement pas. Laissez-moi lancer sa solution dans une classe réutilisable. Puisque BackgroundWorker n'est pas scellé, nous pouvons en déduire notre classe:

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

avec des drapeaux et un verrouillage approprié, nous nous assurons que _resetEvent.WaitOne() ne soit vraiment appelé que si du travail a été commencé, sinon _resetEvent.Set(); pourrait ne jamais être appelé!

Le try-finally assure que _resetEvent.Set(); sera appelé, même si une exception devrait se produire dans notre DoWork-handler. Sinon, l'application pourrait geler à jamais en appelant CancelSync !

nous l'utiliserions comme ceci:

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

vous pouvez également ajouter un handler à l'événement RunWorkerCompleted comme montré ici:

      Classe BackgroundWorker (Microsoft documentation) .

0
répondu Olivier Jacot-Descombes 2014-06-27 19:43:12

fermer le formulaire ferme mon fichier journal ouvert. Mon travailleur d'arrière-plan écrit ce fichier log, donc je ne peux pas laisser MainWin_FormClosing() finir jusqu'à ce que mon travailleur d'arrière-plan se termine. Si je n'attends pas le licenciement de mon travailleur d'arrière-plan, des exceptions se produisent.

Pourquoi est-ce si difficile?

un simple Thread.Sleep(1500) fonctionne, mais il retarde l'arrêt (si trop long), ou provoque des exceptions (si trop court).

fermé juste après l'arrière-plan travailleur se termine, il suffit d'utiliser une variable. Cela fonctionne pour moi:

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.)
0
répondu A876 2017-04-28 23:36:27

vous pouvez vous retirer de L'événement Runworker terminé. Même si vous avez déjà ajouté un gestionnaire d'événements pour _worker, vous pouvez ajouter de l'autre, elles s'exécutent dans l'ordre dans lequel ils ont été ajoutés.

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

cela pourrait être utile si vous avez plusieurs raisons pour lesquelles une annulation peut se produire, ce qui rend la logique d'un gestionnaire D'exécution simple plus compliquée que vous voulez. Par exemple, annuler lorsqu'un utilisateur essaie de fermer le formulaire:

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (_worker != null)
    {
        _worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler((sender3, e3) => this.Close());
        _worker.CancelAsync();
        e.Cancel = true;
    }
}
0
répondu Shawn Rubie 2017-11-27 22:56:45

Oh mec, certains d'entre eux sont devenus ridiculement complexes. tout ce que vous devez faire est de vérifier le travailleur de fond.Annulationpending property à l'intérieur du Gestionnaire DoWork. vous pouvez le vérifier en tout temps. une fois que c'est en attente, mettez E. Cancel = True et bail de la méthode.

// la méthode ici private void Worker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bw = (expéditeur BackgroundWorker);

// do stuff

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

// do other stuff

}

-2
répondu 2008-09-24 19:57:11