Comment attendre l'annulation d'un BackgroundWorker?


125

Considérez une méthode hypothétique d'un objet qui fait des 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 attendre qu'un BackgroundWorker soit terminé?


Dans le passé, les gens ont essayé:

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

Mais ce blocage , car il IsBusyn'est résolu qu'après la RunWorkerCompletedgestion de l' événement, et cet événement ne peut pas être géré tant que l'application n'est pas inactive. L'application ne restera pas inactive tant que le worker n'aura pas terminé. (De plus, c'est une boucle occupée - dégoûtant.)

D'autres ont ajouté des suggestions de kludging dans:

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

Le problème avec cela est que cela Application.DoEvents()provoque le traitement des messages actuellement dans la file d'attente, ce qui provoque des problèmes de réentrée (.NET n'est pas rentrant).

J'espère utiliser une solution impliquant des objets de synchronisation d'événements, où le code attend un événement - que les RunWorkerCompletedgestionnaires d'événements du travailleur définissent. 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 à l'impasse: le gestionnaire d'événements ne peut pas s'exécuter tant que l'application n'est pas inactive, et l'application ne sera pas inactive car elle attend un événement.

Alors, comment pouvez-vous attendre qu'un BackgroundWorker se termine?


Mise à jour Les gens semblent confus par cette question. Ils semblent penser que j'utiliserai BackgroundWorker comme:

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

Ce n'est pas ça, ce n'est pas ce que je fais et ce n'est pas ce qu'on demande ici. Si tel était le cas, il ne servirait à rien d'utiliser un travailleur d'arrière-plan.

Réponses:


130

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
}

7
Cela bloquera l'interface utilisateur (par exemple, pas de repeintes) si le travailleur d'arrière-plan prend beaucoup de temps à annuler. Il est préférable d'utiliser l'un des éléments suivants pour arrêter l'interaction avec l'interface utilisateur: stackoverflow.com/questions/123661/…
Joe

1
+1 exactement ce que le médecin a ordonné ... même si je suis d'accord avec @Joe si la demande d'annulation peut prendre plus d'une seconde.
dotjoe

Que se passe-t-il lorsque CancelAsync est géré avant WaitOne? Ou est-ce que le bouton Annuler ne fonctionne qu'une seule fois.
CodingBarfield

6
J'avais besoin de vérifier le ((BackgroundWorker)sender).CancellationPendingpour obtenir l'événement d'annulation
Luuk

1
Comme mentionné par Luuk, ce n'est pas la propriété Cancel qui doit être vérifiée mais CancellationPending. Deuxièmement, comme le mentionne Joel Coehoorn, attendre que le fil se termine va à l'encontre de l'objectif d'utiliser un fil en premier lieu.
Captain Sensible

15

Il y a un problème avec cette réponse. L'interface utilisateur doit continuer à traiter les messages pendant que vous attendez, sinon elle ne sera pas repeinte, ce qui posera un problème si votre agent d'arrière-plan met beaucoup de temps à répondre à la demande d'annulation.

Une seconde faille est qu'elle _resetEvent.Set()ne sera jamais appelée si le thread de travail lève une exception - laissant le thread principal en attente indéfiniment - cependant cette faille pourrait facilement être corrigée avec un bloc try / finally.

Une façon de faire est d'afficher une boîte de dialogue modale qui a un minuteur qui vérifie à plusieurs reprises si le travailleur d'arrière-plan a terminé son travail (ou a terminé l'annulation dans votre cas). Une fois le travail en arrière-plan terminé, la boîte de dialogue modale redonne le contrôle à votre application. L'utilisateur ne peut pas interagir avec l'interface utilisateur tant que cela ne se produit pas.

Une autre méthode (en supposant que vous ayez au maximum une fenêtre non modale ouverte) consiste à définir ActiveForm.Enabled = false, puis à effectuer une boucle sur Application, DoEvents jusqu'à ce que le travail en arrière-plan ait terminé l'annulation, après quoi vous pouvez redéfinir ActiveForm.Enabled = true.


5
Cela peut être un problème, mais cela est fondamentalement accepté dans le cadre de la question «Comment attendre l' annulation d'un BackgroundWorker». Attendre signifie que vous attendez, vous ne faites rien d'autre. Cela inclut également le traitement des messages. Si je ne voulais pas attendre le travailleur d'arrière-plan, vous pouvez simplement appeler .CancelAsync. Mais ce n'est pas l'exigence de conception ici.
Ian Boyd le

1
+1 pour indiquer la valeur de la méthode CancelAsync, versets en attente de l'agent d'arrière-plan.
Ian Boyd le

10

Presque tous sont déconcertés par la question et ne comprennent pas comment un travailleur est utilisé.

Considérez un gestionnaire d'événements 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;
}

Et tout va bien.

Vient maintenant une situation où l'appelant doit abandonner 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 ouvrir 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 devons vidanger la fusée, mais ce n'est pas autorisé pendant un compte à rebours:

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

    if (rocketOnPad)
        OpenFuelValves();
}

Sans la possibilité d'attendre l'annulation d'un worker, nous devons déplacer les trois méthodes vers 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 pourrais écrire mon code comme ça, mais je ne vais pas le faire. Je m'en fiche, je ne le suis tout simplement pas.


18
Où est la méthode WaitForWorkerToFinish? un code source complet?
Kiquenet du

4

Vous pouvez archiver le RunWorkerCompletedEventArgs dans le RunWorkerCompletedEventHandler pour voir quel était l'état. Succès, annulation ou erreur.

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

Mise à jour : pour voir si votre worker a appelé .CancelAsync () en utilisant ceci:

if (_worker.CancellationPending)
{
    Console.WriteLine("Cancellation is pending, no need to call CancelAsync again");
}

2
La condition est que CancelDoingStuff () ne peut pas revenir tant que le worker n'est pas terminé. Vérifier comment il s'est terminé n'a vraiment pas d'intérêt.
Ian Boyd

Ensuite, vous devrez créer un événement. Cela n'a vraiment rien à voir avec BackgroundWorker en particulier, il vous suffit d'implémenter un événement, de l'écouter et de le déclencher une fois terminé. Et RunWorkerCompletedEventHandler est quand c'est fait. Déclenchez un autre événement.
Seb Nilsson

4

Vous n'attendez pas que l'agent d'arrière-plan se termine. Cela va à peu près à l'encontre de l'objectif de lancer un thread séparé. Au lieu de cela, vous devez laisser votre méthode se terminer et déplacer tout code qui dépend de l'achèvement vers un autre endroit. Vous laissez le travailleur vous dire quand c'est fait et appelez ensuite tout code restant.

Si vous voulez attendre que quelque chose se termine, utilisez une construction de thread différente qui fournit un WaitHandle.


1
Pouvez-vous en suggérer un? Un travailleur d'arrière-plan semble être la seule construction de thread qui peut envoyer des notifications au thread qui a construit l'objet BackgroundWorker.
Ian Boyd

Le backgroundworker est une construction d' interface utilisateur . Il utilise simplement un événement dont votre propre code a encore besoin pour savoir comment appeler. Rien ne vous empêche de créer vos propres délégués pour cela.
Joel Coehoorn le

3

Pourquoi ne pouvez-vous pas simplement vous lier à l'événement BackgroundWorker.RunWorkerCompleted. C'est un rappel qui "se produira lorsque l'opération d'arrière-plan est terminée, a été annulée ou a déclenché une exception."


1
Parce que la personne qui utilise l'objet DoesStuff lui a demandé d'annuler. Les ressources partagées que l'objet utilise sont sur le point d'être déconnectées, supprimées, éliminées, fermées et il doit savoir que c'est fait pour que je puisse continuer.
Ian Boyd

1

Je ne comprends pas pourquoi vous voudriez attendre la fin d'un BackgroundWorker; cela semble vraiment être l'exact opposé de la motivation de la classe.

Cependant, vous pouvez démarrer chaque méthode avec un appel à worker.IsBusy et les faire quitter si elle est en cours d'exécution.


Mauvais choix de mots; je n'attends pas qu'il se termine.
Ian Boyd

1

Je veux 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 en boucle, ma solution était beaucoup plus facile que toutes ces autres choses ^^

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

Je me suis juste dit que je partagerais parce que c'est là que je me suis retrouvé en cherchant une solution. En outre, c'est mon premier article sur le débordement de pile, donc si c'est mauvais ou quoi que ce soit, j'adorerais les critiques! :)


0

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

Le backgroundworker appelle l'événement WorkerCompleted une fois que sa `` méthode de travail '' (la méthode / fonction / sous qui gère l' événement backgroundworker.doWork ) est terminée, il n'est donc pas nécessaire de vérifier si le BW est toujours en cours d'exécution. Si vous souhaitez arrêter votre worker, vérifiez la propriété d'annulation en attente dans votre «méthode de travail».


Je comprends que le travailleur doit surveiller la propriété CancellationPending et sortir dès que possible. Mais comment la personne à l'extérieur, qui a demandé l'annulation de l'agent d'arrière-plan, attend-elle que cela soit fait?
Ian Boyd

L'événement WorkCompleted se déclenchera toujours.
Joel Coehoorn

"Mais comment la personne à l'extérieur, qui a demandé l'annulation de l'agent d'arrière-plan, attend-elle que cela soit fait?"
Ian Boyd

Vous attendez que cela soit fait en attendant que l'événement WorkCompleted se déclenche. Si vous souhaitez empêcher l'utilisateur d'avoir un clic sur toute votre interface graphique, vous pouvez utiliser l'une des solutions proposées par @Joe dans sa réponse ci-dessus (désactiver le formulaire ou afficher quelque chose de modal). En d'autres termes, laissez la boucle inactive du système vous attendre; il vous dira quand il est terminé (en déclenchant l'événement).
Geoff

0

Le flux de travail d'un BackgroundWorkerobjet vous oblige essentiellement à gérer l' RunWorkerCompletedévénement pour les cas d'utilisation d'exécution normale et d'annulation de l'utilisateur. C'est pourquoi la propriété RunWorkerCompletedEventArgs.Cancelled existe. Fondamentalement, cela nécessite que vous considériez votre méthode Cancel 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 ne voulez vraiment pas que votre méthode se termine, je vous suggère de mettre un drapeau comme un AutoResetEventsur un dérivé BackgroundWorker, puis de le remplacer OnRunWorkerCompletedpour définir le drapeau. C'est encore un peu kludgy cependant; Je recommanderais de traiter l'événement d'annulation comme une méthode asynchrone et de faire tout ce qu'il fait actuellement dans le RunWorkerCompletedgestionnaire.


Déplacer le code vers RunWorkerCompleted n'est pas à sa place et n'est pas joli.
Ian Boyd

0

Je suis un peu en retard à la fête ici (environ 4 ans) mais qu'en est-il de la configuration d'un thread asynchrone qui peut gérer une boucle occupée sans verrouiller l'interface utilisateur, puis que le rappel de ce thread soit la confirmation que 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
    }
}

En gros, cela déclenche un autre thread à exécuter en arrière-plan qui attend juste dans sa boucle occupée pour voir si le MyWorkerest terminé. Une fois l' MyWorkerannulation terminée, le thread se terminera et nous pouvons l'utiliser AsyncCallbackpour exécuter la méthode dont nous avons besoin pour suivre l'annulation réussie - cela fonctionnera comme un événement psuedo. Comme il est distinct du thread d'interface utilisateur, il ne verrouille pas l'interface pendant que nous attendons la MyWorkerfin de l'annulation. Si votre intention est vraiment de verrouiller et d'attendre l'annulation, cela ne vous sert à rien, mais si vous voulez juste attendre pour pouvoir démarrer un autre processus, cela fonctionne bien.


0

Je sais que c'est vraiment tard (5 ans) mais ce que vous cherchez c'est d'utiliser un Thread et un SynchronizationContext . Vous allez devoir rassembler les appels d'interface utilisateur vers le thread d'interface utilisateur "à la main" plutôt que de laisser le Framework le faire automatiquement.

Cela vous permet d'utiliser un fil de discussion que vous pouvez attendre si nécessaire.


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

Quelle est votre question? Dans SO, vous devez être précis lorsque vous posez des questions
Alma Do

@C'est une réponse pas une question.
Code L du

0

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 tout simplement ne pas fonctionner. Permettez-moi de transformer sa solution en une classe réutilisable. Puisqu'il BackgroundWorkern'est pas scellé, nous pouvons en dériver 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()cela n'est vraiment appelé que si un travail a été commencé, sinon il _resetEvent.Set();pourrait ne jamais être appelé!

Le try-finally garantit qu'il _resetEvent.Set();sera appelé, même si une exception doit se produire dans notre gestionnaire DoWork. Sinon, l'application pourrait se figer pour toujours lors de l'appel 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 gestionnaire à l' RunWorkerCompletedévénement comme indiqué ici:
     BackgroundWorker Class (documentation Microsoft) .


0

La fermeture du formulaire ferme mon fichier journal ouvert. Mon travailleur d'arrière-plan écrit ce fichier journal, je ne peux donc pas laisser MainWin_FormClosing()terminer jusqu'à ce que mon travailleur d'arrière-plan se termine. Si je n'attends pas que mon agent d'arrière-plan se termine, des exceptions se produisent.

Pourquoi est-ce si difficile?

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

Pour arrêter juste après la fin de l'outil de travail en arrière-plan, utilisez simplement 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

Vous pouvez utiliser l'événement RunWorkerCompleted. Même si vous avez déjà ajouté un gestionnaire d'événements pour _worker, vous pouvez en ajouter un autre qu'ils exécuteront 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 peut être utile si vous avez plusieurs raisons pour lesquelles une annulation peut se produire, rendant la logique d'un seul gestionnaire RunWorkerCompleted plus compliquée que vous ne le souhaitez. Par exemple, annulation lorsqu'un utilisateur tente de fermer le formulaire:

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

0

J'utilise la asyncméthode et awaitj'attends que l'ouvrier termine son travail:

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

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

et en DoWorkméthode:

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

Vous pouvez également encapsuler la whileboucle DoWorkavec try ... catchpour définir _isBusyest falsesur exception. Ou, enregistrez simplement _worker.IsBusyla StopAsyncboucle while.

Voici un exemple de mise en œuvre complète:

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

Pour arrêter le worker et attendre qu'il s'exécute jusqu'à la fin:

await myBackgroundWorker.StopAsync();

Les problèmes avec cette méthode sont:

  1. Vous devez utiliser des méthodes asynchrones jusqu'au bout.
  2. wait Task.Delay est inexact. Sur mon PC, Task.Delay (1) attend en fait ~ 20 ms.

-2

oh mec, certains d'entre eux sont devenus ridiculement complexes. tout ce que vous avez à faire est de vérifier la propriété BackgroundWorker.CancellationPending dans le gestionnaire DoWork. vous pouvez le vérifier à tout moment. une fois qu'il est en attente, définissez e.Cancel = True et libérez la méthode.

// méthode ici private void Worker_DoWork (expéditeur d'objet, DoWorkEventArgs e) {BackgroundWorker bw = (expéditeur en tant que BackgroundWorker);

// do stuff

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

// do other stuff

}


1
Et avec cette solution, comment la personne qui a appelé CancelAsync est-elle obligée d'attendre l'annulation de l'agent d'arrière-plan?
Ian Boyd
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.