web-dev-qa-db-fra.com

Comment arrêter correctement BackgroundWorker


J'ai un formulaire avec 2 comboboxes dessus. Et je veux remplir combobox2.DataSource sur la base de combobox1.Text et combobox2.Text (je suppose que l'utilisateur a terminé la saisie dans combobox1 et qu'il est en train de l'écrire dans combobox2). J'ai donc un gestionnaire d'événements pour combobox2 comme ceci:

private void combobox2_TextChanged(object sender, EventArgs e)
{
    if (cmbDataSourceExtractor.IsBusy)
       cmbDataSourceExtractor.CancelAsync();

    var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
       V2 = combobox2.Text};
    cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
}

Dans la mesure où la création de DataSource est un processus fastidieux (elle crée une requête vers la base de données et l'exécute), j'ai donc décidé qu'il était préférable de l'exécuter dans un autre processus utilisant BackgroundWorker. Il y a donc un scénario où cmbDataSourceExtractor n'a pas terminé son travail et que l'utilisateur tape un symbole supplémentaire. Dans ce cas, je reçois une exception sur cette ligne
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues ); à propos de ce BackgroundWorker est occupé et ne peut pas effectuer plusieurs actions en même temps.
Comment se débarrasser de cette exception?
Merci d'avance!

55
StuffHappens

CancelAsync n'abandonne pas réellement votre thread ou quoi que ce soit du genre. Il envoie un message au thread de travail indiquant que le travail doit être annulé via BackgroundWorker.CancellationPending. Votre délégué DoWork exécuté en arrière-plan doit vérifier périodiquement cette propriété et gérer l'annulation elle-même.

La difficulté réside dans le fait que votre délégué DoWork est probablement en train de bloquer, ce qui signifie que le travail que vous effectuez sur votre source de données doit être terminé avant de pouvoir effectuer quoi que ce soit d'autre (comme le contrôle d'AnnulationPending). Vous devrez peut-être déplacer votre travail réel vers un autre délégué asynchrone (ou peut-être encore mieux, soumettez le travail à ThreadPool), et demandez à votre thread de travail principal d'interroger jusqu'à ce que ce thread de travail interne déclenche un état d'attente, OR détecte CancellationPending.

http://msdn.Microsoft.com/en-us/library/system.componentmodel.backgroundworker.cancelasync.aspx

http://www.codeproject.com/KB/cpp/BackgroundWorker_Threads.aspx

87
HackedByChinese

Si vous ajoutez une boucle entre CancelAsync () et RunWorkerAsync (), cela résoudra votre problème.

 private void combobox2_TextChanged(object sender, EventArgs e)
 {
     if (cmbDataSourceExtractor.IsBusy)
        cmbDataSourceExtractor.CancelAsync();

     while(cmbDataSourceExtractor.IsBusy)
        Application.DoEvents();

     var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
        V2 = combobox2.Text};
     cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
  }

La boucle while avec l'appel à Application.DoEvents () interrompra l'exécution de votre nouveau thread de travail jusqu'à ce que celui-ci soit correctement annulé. Gardez à l'esprit que vous devez toujours gérer l'annulation de votre thread de travail. Avec quelque chose comme:

 private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e)
 {
      if (this.cmbDataSourceExtractor.CancellationPending)
      {
          e.Cancel = true;
          return;
      }
      // do stuff...
 }

Application.DoEvents () dans le premier extrait de code continuera à traiter votre file d'attente de messages de threads d'interface graphique afin que même d'annuler et de mettre à jour la propriété cmbDataSourceExtractor.IsBusy soit toujours traitée (si vous avez simplement ajouté une continuation au lieu d'Application.DoEvents la boucle verrouillerait le thread d'interface graphique dans un état occupé et ne traiterait pas l'événement pour mettre à jour le cmbDataSourceExtractor.IsBusy)

26
jenovachild

Vous devrez utiliser un indicateur partagé entre le thread principal et BackgroundWorker, tel que BackgroundWorker.CancellationPending. Lorsque vous souhaitez que BackgroundWorker se ferme, définissez simplement l'indicateur à l'aide de BackgroundWorker.CancelAsync ().

MSDN en a un exemple: http://msdn.Microsoft.com/en-us/library/system.componentmodel.backgroundworker.cancellationpending.aspx

6
Daniel Gehriger

MON exemple DoWork est ci-dessous:

    DoLengthyWork();

    //this is never executed
    if(bgWorker.CancellationPending)
    {
        MessageBox.Show("Up to here? ...");
        e.Cancel = true;
    }

dans DoLenghtyWork:

public void DoLenghtyWork()
{
    OtherStuff();
    for(int i=0 ; i<10000000; i++) 
    {  int j = i/3; }
}

dans OtherStuff ():

public void OtherStuff()
{
    for(int i=0 ; i<10000000; i++) 
    {  int j = i/3; }
}

Ce que vous voulez faire, c'est modifier DoLenghtyWork et OtherStuff () afin qu'ils deviennent:

public void DoLenghtyWork()
{
    if(!bgWorker.CancellationPending)
    {              
        OtherStuff();
        for(int i=0 ; i<10000000; i++) 
        {  
             int j = i/3; 
        }
    }
}

public void OtherStuff()
{
    if(!bgWorker.CancellationPending)
    {  
        for(int i=0 ; i<10000000; i++) 
        {  
            int j = i/3; 
        }
    }
}
3
kawa

Ma réponse est un peu différente car j'ai essayé ces méthodes mais elles ne fonctionnaient pas. Mon code utilise une classe supplémentaire qui recherche un indicateur booléen dans une classe statique publique lors de la lecture des valeurs de la base de données ou lorsque je le préfère juste avant l'ajout d'un objet à un objet List ou quelque chose en tant que tel. Voir le changement dans le code ci-dessous. J'ai ajouté la propriété ThreadWatcher.StopThread. pour cette explication, je ne vais pas réintégrer le fil actuel, car ce n'est pas votre problème, mais c'est aussi simple que de définir la propriété sur false avant d'accéder au prochain fil ... 

private void combobox2_TextChanged(object sender, EventArgs e)
 {
  //Stop the thread here with this
     ThreadWatcher.StopThread = true;//the rest of this thread will run normally after the database function has stopped.
     if (cmbDataSourceExtractor.IsBusy)
        cmbDataSourceExtractor.CancelAsync();

     while(cmbDataSourceExtractor.IsBusy)
        Application.DoEvents();

     var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
        V2 = combobox2.Text};
     cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
  }

tout va bien

private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e)
 {
      if (this.cmbDataSourceExtractor.CancellationPending)
      {
          e.Cancel = true;
          return;
      }
      // do stuff...
 }

Ajoutez maintenant la classe suivante

public static class ThreadWatcher
{
    public static bool StopThread { get; set; }
}

et dans votre classe où vous lisez la base de données

List<SomeObject>list = new List<SomeObject>();
...
if (!reader.IsDbNull(0))
    something = reader.getString(0);
someobject = new someobject(something);
if (ThreadWatcher.StopThread == true)
    break;
list.Add(something);
...

n'oubliez pas d'utiliser un bloc finally pour fermer correctement votre connexion à la base de données, etc. J'espère que cela vous aidera! S'il vous plaît marquez-moi si vous le trouvez utile.

1
Stanley

Le problème est dû au fait que cmbDataSourceExtractor.CancelAsync() est une méthode asynchrone. L'opération Cancel n'est pas encore terminée lorsque cmdDataSourceExtractor.RunWorkerAsync(...) quitte. Vous devez attendre que cmdDataSourceExtractor soit terminé avant d'appeler à nouveau RunWorkerAsync. Comment faire cela est expliqué dans cette SO question .

1
Bas Bossink

Dans mon cas, j'ai dû regrouper la base de données pour que la confirmation de paiement arrive, puis mettre à jour l'interface utilisateur WPF.

Mécanisme qui accélère tous les processus:

public void Execute(object parameter)
        {
            try
            {
                var amount = ViewModel.Amount;
                var transactionId = ViewModel.TransactionMain.TransactionId.ToString();
                var productCode = ViewModel.TransactionMain.TransactionDetailList.First().Product.ProductCode;
                var transactionReference = GetToken(amount, transactionId, productCode);
                var url = string.Format("{0}New?transactionReference={1}", Settings.Default.PaymentUrlWebsite, transactionReference);
                Process.Start(new ProcessStartInfo(url));
                ViewModel.UpdateUiWhenDoneWithPayment = new BackgroundWorker {WorkerSupportsCancellation = true};
                ViewModel.UpdateUiWhenDoneWithPayment.DoWork += ViewModel.updateUiWhenDoneWithPayment_DoWork;
                ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerCompleted += ViewModel.updateUiWhenDoneWithPayment_RunWorkerCompleted;
                ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerAsync();
            }
            catch (Exception e)
            {
                ViewModel.Log.Error("Failed to navigate to payments", e);
                MessageBox.Show("Failed to navigate to payments");
            }
        }

Mécanisme qui vérifie l'achèvement:

 private void updateUiWhenDoneWithPayment_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(30000);
        while (string.IsNullOrEmpty(GetAuthToken()) && !((BackgroundWorker)sender).CancellationPending)
        {
            Thread.Sleep(5000);
        }

        //Plug in pooling mechanism
        this.AuthCode = GetAuthToken();
    }

Mécanisme qui annule si la fenêtre se ferme:

private void PaymentView_OnUnloaded(object sender, RoutedEventArgs e)
    {
        var context = DataContext as PaymentViewModel;
        if (context.UpdateUiWhenDoneWithPayment != null && context.UpdateUiWhenDoneWithPayment.WorkerSupportsCancellation && context.UpdateUiWhenDoneWithPayment.IsBusy)
            context.UpdateUiWhenDoneWithPayment.CancelAsync();
    }
0
Matas Vaitkevicius

Je suis d'accord avec les gars. Mais parfois, vous devez ajouter plus de choses.

IE

1) Ajoutez ce worker.WorkerSupportsCancellation = true;

2) Ajoutez à votre classe une méthode pour faire les choses suivantes

public void KillMe()
{
   worker.CancelAsync();
   worker.Dispose();
   worker = null;
   GC.Collect();
}

Donc, avant de fermer votre application, vous devez appeler cette méthode.

3) Probablement, vous pouvez Dispose, null toutes les variables et tous les timers contenus dans la BackgroundWorker

0