BackgroundWorker OnWorkCompleted lève une exception de threading
-
03-07-2019 - |
Question
J'ai un simple UserControl pour la pagination de base de données, qui utilise un contrôleur pour effectuer les appels DAL réels. J'utilise BackgroundWorker
pour effectuer le gros travail, et sur l'événement OnWorkCompleted
, je réactive certains boutons, modifie une propriété TextBox.Text
et déclencher un événement pour le formulaire parent.
Le formulaire A contient mon UserControl. Lorsque je clique sur un bouton qui ouvre le formulaire B, même si je ne fais rien "là-bas" et fermez-le, et essayez d’apporter la page suivante de ma base de données, le OnWorkCompleted
est appelé sur le thread de travail (et non sur mon thread principal), et lève une exception cross-thread.
Pour le moment, j'ai ajouté une coche pour InvokeRequired
dans le gestionnaire, mais le point entier de OnWorkCompleted
ne doit-il pas être appelé sur le thread principal? Pourquoi cela ne fonctionnerait-il pas comme prévu?
EDIT:
J'ai réussi à limiter le problème à arcgis et à BackgroundWorker
. J'ai la solution suivante qui ajoute une commande à arcmap, qui ouvre un simple Form1
avec deux boutons.
Le premier bouton exécute un BackgroundWorker
qui dort 500 ms et met à jour un compteur.
Dans la méthode RunWorkerCompleted
, il recherche InvokeRequired
et met à jour le titre pour indiquer si la méthode était exécutée à l'origine dans le thread principal ou dans le thread de travail.
Le deuxième bouton ouvre simplement Form2
, qui ne contient rien.
Au début, tous les appels à RunWorkerCompletedare
sont passés à l'intérieur du thread principal (Comme prévu - c'est le point le plus lourd de la méthode RunWorkerComplete, Du moins d'après ce que j'ai compris de MSDN sur BackgroundWorker
)
Après avoir ouvert et fermé Form2
, RunWorkerCompleted
est toujours appelé sur le thread de travail. Je veux ajouter que je peux simplement laisser cette solution au problème telle quelle (vérifiez InvokeRequired
dans la méthode RunWorkerCompleted
), mais je veux comprendre pourquoi cela se produit. mes attentes. Dans mon " réel " code J'aimerais toujours savoir que la méthode RunWorkerCompleted
est appelée sur le thread principal.
J'ai réussi à identifier le problème au niveau de la commande form.Show ();
dans mon BackgroundTesterBtn
- si j'utilise ShowDialog ()
au lieu de cela, je n’ai aucun problème ( RunWorkerCompleted
s’exécute toujours sur le thread principal). J'ai besoin d'utiliser Show ()
dans mon projet ArcMap, afin que l'utilisateur ne soit pas lié au formulaire.
J'ai également essayé de reproduire le bogue sur un projet WinForms normal. J'ai ajouté un projet simple qui ouvre simplement le premier formulaire sans ArcMap, mais dans ce cas, je ne pouvais pas reproduire le bogue - le RunWorkerCompleted
était exécuté sur le thread principal, que j'avais utilisé Show () ou non.
ou ShowDialog ()
, avant et après l'ouverture de Form2
. J'ai essayé d'ajouter un troisième formulaire pour qu'il agisse comme formulaire principal avant mon Form1
, mais cela n'a pas modifié le résultat.
Voici mon sln simple (VS2005sp1) - il est nécessaire de
ESRI.ArcGIS.ADF (9.2.4.1420)
ESRI.ArcGIS.ArcMapUI (9.2.3.1380)
ESRI.ArcGIS.SystemUI (9.2.3.1380)
La solution
Cela ressemble à un bug:
http://connect.microsoft.com/VisualStudio/feedback /ViewFeedback.aspx?FeedbackID=116930
http://thedatafarm.com/devlifeblog/archive/2005 /12/21/39532.aspx
Je suggère donc d'utiliser le pare-balles (pseudocode):
if(control.InvokeRequired)
control.Invoke(Action);
else
Action()
Autres conseils
Le point entier de
OnWorkCompleted
ne doit-il pas être appelé sur le thread principal? Pourquoi cela ne fonctionnerait-il pas comme prévu?
Non, ce n'est pas.
Vous ne pouvez pas simplement lancer une vieille chose sur un vieux fil. Les threads ne sont pas des objets polis que vous pouvez simplement dire "exécutez ceci, s'il vous plaît".
Un meilleur modèle mental de fil est un train de marchandises. Une fois que c'est parti, c'est sur sa propre piste. Vous ne pouvez pas changer de cap ou l'arrêter. Si vous voulez l’influencer, vous devez attendre jusqu’à ce qu’elle arrive à la gare suivante (par exemple: le faire vérifier manuellement pour certains événements) ou le faire dérailler ( Thread.Abort
et les exceptions CrossThread ont presque les mêmes conséquences que de faire dérailler un train ... méfiez-vous!).
Les contrôles Winforms prennent en charge ce problème avec une sorte (ils ont Control.BeginInvoke
qui vous permet d'exécuter n'importe quelle fonction sur le thread d'interface utilisateur), mais cela ne fonctionne que parce qu'ils ont crochet spécial dans la pompe de message de l'interface utilisateur Windows et écrivez des gestionnaires spéciaux. Pour reprendre l’analogie ci-dessus, leur train s’enregistre à la gare et recherche périodiquement de nouvelles indications. Vous pouvez utiliser cette fonction pour l’afficher vous-même.
BackgroundWorker
est conçu pour un usage général (il ne peut pas être lié à l'interface graphique de Windows) et ne peut donc pas utiliser les fonctionnalités Windows Control.BeginInvoke
. Il doit supposer que votre fil principal est un "train" imparable qui agit comme il se doit, donc l'événement terminé doit s'exécuter dans le fil de travail ou pas du tout.
Toutefois, lorsque vous utilisez Winforms, dans votre gestionnaire OnWorkCompleted
, vous pouvez demander à la fenêtre d'exécuter un autre rappel à l'aide du BeginInvoke
. fonctionnalité que j'ai mentionnée ci-dessus. Comme ceci:
// Assume we're running in a windows forms button click so we have access to the
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
var b = new BackgroundWorker();
b.DoWork += ... blah blah
// attach an anonymous function to the completed event.
// when this function fires in the worker thread, it will ask the form (this)
// to execute the WorkCompleteCallback on the UI thread.
// when the form has some spare time, it will run your function, and
// you can do all the stuff that you want
b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
b.RunWorkerAsync(); // GO!
}
void WorkCompleteCallback()
{
Button.Enabled = false;
//other stuff that only works in the UI thread
}
Votre gestionnaire d'événements RunWorkerCompleted doit toujours vérifier les propriétés Error et Canceled avant d'accéder à la propriété Result. Si une exception a été déclenchée ou si l'opération a été annulée, l'accès à la propriété Result déclenche une exception.
BackgroundWorker
vérifie si l'instance de délégué pointe vers une classe qui prend en charge l'interface ISynchronizeInvoke
. Votre couche DAL n'implémente probablement pas cette interface. Normalement, vous utiliseriez BackgroundWorker
sur un Form
, qui prend en charge cette interface.
Si vous souhaitez utiliser le BackgroundWorker
de la couche DAL et souhaitez mettre à jour l'interface utilisateur à partir de cet emplacement, vous avez trois options:
- vous voudriez continuer à appeler la méthode
Invoke
- implémente l'interface
ISynchronizeInvoke
sur la classe DAL et redirige les appels manuellement (il ne s'agit que de trois méthodes et d'une propriété) - avant d'appeler
BackgroundWorker
(donc sur le thread d'interface utilisateur), pour appelerSynchronizationContext.Current
et pour enregistrer l'instance de contenu dans une variable d'instance. LeSynchronizationContext
vous donnera ensuite la méthodeSend
, qui fera exactement ce queInvoke
fait.
La meilleure approche pour éviter les problèmes de cross-threading dans l'interface graphique consiste à utilise SynchronizationContext .