Je suis toujours embêté par le threading en arrière-plan dans une interface utilisateur WinForm. Pourquoi ? Voici quelques-uns des problèmes :
- De toute évidence, le problème le plus important est que je ne peux pas modifier un contrôle à moins d'être exécuté sur le même thread que celui qui l'a créé.
- Comme vous le savez, Invoke, BeginInvoke, etc. ne sont pas disponibles avant la création d'un contrôle.
- Même si RequiresInvoke renvoie vrai, BeginInvoke peut toujours lancer ObjectDisposed et même s'il ne le fait pas, il peut ne jamais exécuter le code si le contrôle est détruit.
- Même après le retour de RequiresInvoke, Invoke peut attendre indéfiniment l'exécution par un contrôle qui a été disposé en même temps que l'appel à Invoke.
Je suis à la recherche d'une solution élégante à ce problème, mais avant d'entrer dans les détails de ce que je recherche, je pensais clarifier le problème. Il s'agit de prendre le problème générique et de lui donner un exemple plus concret. Dans cet exemple, disons que nous transférons de grandes quantités de données sur Internet. L'interface utilisateur doit être capable de montrer un dialogue de progression pour le transfert en cours. La boîte de dialogue de progression doit se mettre à jour constamment et rapidement (5 à 20 mises à jour par seconde). L'utilisateur peut rejeter la boîte de dialogue de progression à tout moment et la rappeler si nécessaire. En outre, supposons que si la boîte de dialogue est visible, elle doit traiter chaque événement de progression. L'utilisateur peut cliquer sur Annuler dans la boîte de dialogue de progression et, en modifiant les arguments de l'événement, annuler l'opération.
J'ai maintenant besoin d'une solution qui s'inscrive dans le cadre des contraintes suivantes :
- Permettre à un fil de travail d'appeler une méthode sur un contrôle/forme et bloquer/attendre jusqu'à ce que l'exécution soit terminée.
- Permettre à la boîte de dialogue elle-même d'appeler cette même méthode lors de l'initialisation ou autre (et donc ne pas utiliser invoke).
- Ne faites pas peser la charge de la mise en œuvre sur la méthode de traitement ou l'événement appelant, la solution ne doit modifier que l'abonnement à l'événement lui-même.
- Gérer de manière appropriée les appels bloquants à une boîte de dialogue qui pourrait être en cours d'élimination. Malheureusement, cela n'est pas aussi simple que de vérifier IsDisposed.
- Doit pouvoir être utilisé avec tout type d'événement (supposez un délégué de type EventHandler)
- Ne doit pas traduire les exceptions en TargetInvocationException.
- La solution doit fonctionner avec .Net 2.0 et plus.
Alors, est-ce que cela peut être résolu compte tenu des contraintes ci-dessus ? J'ai cherché et creusé dans d'innombrables blogs et discussions, mais hélas je n'ai toujours rien trouvé.
Mise à jour : Je réalise que cette question n'a pas de réponse facile. Je ne suis sur ce site que depuis quelques jours et j'ai vu des personnes ayant beaucoup d'expérience répondre à des questions. J'espère que l'une de ces personnes a résolu le problème suffisamment pour que je n'aie pas à consacrer la semaine ou plus qu'il faudra pour construire une solution raisonnable.
Mise à jour n°2 : Ok, je vais essayer de décrire le problème de manière un peu plus détaillée et voir ce qui se passe (si quelque chose se passe). Les propriétés suivantes, qui nous permettent de déterminer l'état de l'objet, ont quelques points d'inquiétude...
-
Control.InvokeRequired = Documenté pour retourner false si l'exécution se fait sur le thread actuel ou si IsHandleCreated retourne false pour tous les parents. Je suis troublé par l'implémentation de InvokeRequired qui a le potentiel de lancer ObjectDisposedException ou même de recréer le handle de l'objet. Et comme InvokeRequired peut renvoyer true lorsque nous ne sommes pas en mesure d'invoquer (Dispose en cours) et qu'il peut renvoyer false même si nous avons besoin d'utiliser invoke (Create en cours), on ne peut tout simplement pas lui faire confiance dans tous les cas. Le seul cas que je vois où nous pouvons faire confiance à InvokeRequired pour retourner false est lorsque IsHandleCreated retourne true à la fois avant et après l'appel (BTW les docs MSDN pour InvokeRequired mentionnent la vérification de IsHandleCreated).
-
Control.IsHandleCreated = Renvoie true si un handle a été attribué au contrôle ; sinon, false. Bien que IsHandleCreated soit un appel sûr, il peut tomber en panne si le contrôle est en train de recréer son handle. Ce problème potentiel semble pouvoir être résolu en effectuant un lock(control) pendant l'accès à IsHandleCreated et InvokeRequired.
-
Control.Disposing = Renvoie un message de vérité si le contrôle est en train d'être éliminé.
-
Control.IsDisposed = Renvoie true si le contrôle a été éliminé. J'envisage de souscrire à l'événement Disposed et de vérifier la propriété IsDisposed pour déterminer si le BeginInvoke se terminera un jour. Le gros problème ici est l'absence d'un verrou de synchronisation pendant la transition Disposing -> Disposed. Il est possible que si vous souscrivez à l'événement Disposed et que vous vérifiez ensuite que Disposing == false && IsDisposed == false, vous ne verrez jamais l'événement Disposed se déclencher. Cela est dû au fait que l'implémentation de Dispose définit Disposing = false, puis définit Disposed = true. Cela vous donne une opportunité (même minime) de lire à la fois Disposing et IsDisposed comme faux sur un contrôle disposé.
... j'ai mal à la tête :( J'espère que les informations ci-dessus apporteront un peu plus de lumière à tous ceux qui ont ces problèmes. J'apprécie vos cycles de réflexion sur ce sujet.
On se rapproche du problème... Ce qui suit est la dernière moitié de la méthode Control.DestroyHandle() :
if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
lock (this.threadCallbackList)
{
Exception exception = new ObjectDisposedException(base.GetType().Name);
while (this.threadCallbackList.Count > 0)
{
ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
entry.exception = exception;
entry.Complete();
}
}
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
this.window.DestroyHandle();
}
Vous remarquerez que l'exception ObjectDisposedException est envoyée à toutes les invocations inter-filières en attente. Peu de temps après, l'appel à this.window.DestroyHandle() détruit la fenêtre et définit la référence de son handle à IntPtr.Zero, empêchant ainsi tout nouvel appel à la méthode BeginInvoke (ou plus précisément MarshaledInvoke qui gère à la fois BeginInvoke et Invoke). Le problème ici est qu'après que le verrou se soit libéré sur threadCallbackList, une nouvelle entrée peut être insérée avant que le thread du contrôle ne remette à zéro le handle de la fenêtre. Il semble que ce soit le cas que je rencontre, bien que peu fréquent, assez souvent pour arrêter une libération.
Mise à jour n°4 :
Désolé de faire traîner les choses en longueur, mais j'ai pensé que cela valait la peine d'être documenté ici. J'ai réussi à résoudre la plupart des problèmes ci-dessus et je me rapproche d'une solution qui fonctionne. J'ai rencontré un autre problème qui me préoccupait, mais que je n'avais pas encore vu dans la nature.
Ce problème a à voir avec le génie qui a écrit la propriété Control.Handle :
public IntPtr get_Handle()
{
if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
{
throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
}
if (!this.IsHandleCreated)
{
this.CreateHandle();
}
return this.HandleInternal;
}
En soi, ce n'est pas si mal (quelles que soient mes opinions sur les modifications get { }) ; cependant, lorsqu'il est combiné avec la propriété InvokeRequired ou la méthode Invoke/BeginInvoke, c'est mauvais. Voici le flux de base de la méthode Invoke :
if( !this.IsHandleCreated )
throw;
... do more stuff
PostMessage( this.Handle, ... );
Le problème est qu'à partir d'un autre thread, je peux réussir à passer la première instruction if, après quoi le handle est détruit par le thread du contrôle, ce qui fait que le get de la propriété Handle recrée le handle de la fenêtre sur mon thread. Cela peut ensuite provoquer la levée d'une exception sur le thread du contrôle d'origine. Cette situation me laisse vraiment perplexe car il n'y a aucun moyen de s'en prémunir. S'ils avaient utilisé uniquement la propriété InternalHandle et testé le résultat de IntPtr.Zero, ce ne serait pas un problème.