Pourquoi l'appelant est-il responsable de la sécurité des threads dans la programmation d'interface graphique?

37

J'ai vu, dans de nombreux endroits, qu'il est de bon al avis 1 qu'il incombe à l'appelant de s'assurer que vous êtes sur le fil de l'interface utilisateur lors de la mise à jour des composants de l'interface utilisateur (en particulier dans Java Swing, que vous êtes sur le fil de distribution des événements ). .

Pourquoi cela est-il ainsi? Le thread de répartition d'événement est une préoccupation de la vue dans MVC / MVP / MVVM; le gérer n'importe où, mais la vue crée un couplage étroit entre l'implémentation de la vue et le modèle de threading de cette implémentation.

Spécifiquement, supposons que j’ai une application à architecture MVC qui utilise Swing. Si l'appelant est responsable de la mise à jour des composants sur le thread Event Dispatch, puis, si j'essaie d'échanger mon implémentation Swing View pour une implémentation JavaFX, je dois changer tout le code Presenter / Controller pour qu'il utilise plutôt le thread Application JavaFX .

Donc, je suppose que j'ai deux questions:

  1. Pourquoi l'appelant est-il responsable de la sécurité du thread de composant d'interface utilisateur? Où est la faille dans mon raisonnement ci-dessus?
  2. Comment puis-je concevoir mon application de manière à ce que le couplage de ces problèmes de sécurité des filets soit faible, tout en maintenant la sécurité des filets?

Permettez-moi d'ajouter du code Java MCVE pour illustrer ce que je veux dire par "responsable de l'appelant" (il existe d'autres bonnes pratiques que je ne fais pas mais que j'essaie exprès d'être aussi minimes que possible):

Appelant étant responsable:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

Voir être responsable:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1: L’auteur de ce message a le score le plus élevé dans Swing on Stack Overflow. Il a dit cela partout et j'ai aussi vu que c'était aussi la responsabilité de l'appelant à d'autres endroits.

durron597
la source
1
Eh, la performance à mon humble avis. Ces publications d'événements ne sont pas gratuites, et dans les applications non triviales, vous souhaitez minimiser leur nombre (et vous assurer qu'aucune n'est trop grande), mais cette minimisation / condensation doit logiquement être effectuée dans le présentateur.
Ordous le
1
@Ordous Vous pouvez toujours vous assurer que les écritures sont minimes tout en plaçant le transfert de fil dans la vue.
durron597
2
Il y a quelque temps, j'ai lu un très bon blog qui traitait de cette question. Il dit en substance qu'il est très dangereux d'essayer de sécuriser le fil du kit d'interface utilisateur, car il introduit des blocages possibles et, en fonction de la manière dont il est mis en œuvre, cadre. Il y a aussi une considération de performance. Pas tellement maintenant, mais lorsque Swing a été publié, il a été vivement critiqué pour ses performances (ça a été mauvais), mais ce n’est pas vraiment la faute de Swing, c’est le manque de connaissances des personnes quant à son utilisation.
MadProgrammer
1
SWT applique le concept de sécurité des threads en générant des exceptions si vous le violez, pas beau, mais au moins, vous en êtes conscient. Vous avez mentionné le passage de Swing à JavaFX, mais vous auriez ce problème avec à peu près n'importe quel framework d'interface utilisateur. Swing semble être celui qui met en évidence le problème. Vous pouvez concevoir une couche intermédiaire (contrôleur d’un contrôleur?) Chargée de s’assurer que les appels vers l’UI sont correctement synchronisés. Il est impossible de savoir exactement comment vous pourriez concevoir les parties de votre API qui ne sont pas d'interface utilisateur du point de vue de l'API de l'interface utilisateur
MadProgrammer
1
et la plupart des développeurs se plaignaient du fait que toute protection de thread implémentée dans l'API de l'interface utilisateur était trop restrictive ou ne répondait pas à leurs besoins. Mieux vaut vous permettre de décider comment vous souhaitez résoudre ce problème, en fonction de vos besoins
MadProgrammer

Réponses:

22

Graham Hamilton (un grand architecte de Java) vers la fin de son essai de rêve qui a échoué , indique que si les développeurs "doivent préserver l'équivalence avec un modèle de file d'attente d'événements, ils devront suivre diverses règles non évidentes" et disposer d'une vue visible et explicite. modèle de file d'attente d'événements "semble aider les gens à suivre plus fidèlement le modèle et à construire ainsi des programmes d'interface graphique fonctionnant de manière fiable."

En d'autres termes, si vous essayez de placer une façade multithread au-dessus d'un modèle de file d'attente d'événements, l'abstraction générera parfois des fuites de manière non évidente et extrêmement difficile à déboguer. Il semble que cela fonctionnera sur le papier, mais finira par s'effondrer dans la production.

Ajouter de petits wrappers autour de composants individuels ne posera probablement pas de problème, comme mettre à jour une barre de progression à partir d'un thread de travail. Si vous essayez de faire quelque chose de plus complexe qui nécessite plusieurs verrous, il devient très difficile de raisonner sur la manière dont la couche multithread et la couche de file d'attente d'événements interagissent.

Notez que ces types de problèmes sont universels pour tous les kits d'outils graphiques. Présumer un modèle d'envoi d'événements dans votre présentateur / contrôleur ne vous lie pas étroitement à un seul modèle de concurrence de la boîte à outils de l'interface graphique, mais bien à tous . L’interface de mise en file d’événements ne devrait pas être si difficile à résumer.

Karl Bielefeldt
la source
25

Parce que sécuriser le fil de la librairie graphique est un mal de tête et un goulot d’étranglement.

Le flux de contrôle dans les interfaces graphiques va souvent dans 2 directions: de la file d'attente d'événements à la fenêtre racine jusqu'aux widgets de l'interface graphique et du code d'application au widget propagé jusqu'à la fenêtre racine.

Verrouillage d' élaborer une stratégie qui se verrouille pas la fenêtre racine (causera beaucoup de discorde) est difficile . Verrouiller la partie inférieure pendant qu'un autre thread se verrouille de haut en bas est un excellent moyen de parvenir à une impasse instantanée.

Et vérifier si le thread actuel est chaque fois le fil de l'interface graphique est coûteux et peut créer une confusion quant à ce qui se passe réellement avec l'interface graphique, en particulier lorsque vous effectuez une séquence d'écriture de mise à jour en lecture. Cela doit avoir un verrou sur les données pour éviter les courses.

monstre à cliquet
la source
1
Fait intéressant, je résous ce problème en ayant un cadre de mise en file d'attente pour ces mises à jour que j'ai écrit qui gère le transfert de thread.
durron597
2
@ durron597: Et vous n'avez jamais de mises à jour en fonction de l'état actuel de l'interface utilisateur qu'un autre thread pourrait influencer? Alors ça pourrait marcher.
Deduplicator
Pourquoi auriez-vous besoin de verrous imbriqués? Pourquoi avez-vous besoin de verrouiller toute la fenêtre racine lorsque vous travaillez sur des détails dans une fenêtre enfant? L'ordre de verrouillage est un problème qui peut être résolu en fournissant une seule méthode exposée pour verrouiller plusieurs verrous dans le bon ordre (de haut en bas ou de bas en haut, mais ne laissez pas le choix à l'appelant)
MSalters le
1
@MSalters avec root je voulais dire la fenêtre en cours. Pour obtenir tous les verrous dont vous avez besoin, vous devez accéder à la hiérarchie, ce qui nécessite de verrouiller chaque conteneur lorsque vous le trouvez, puis de déverrouiller (pour vous assurer que vous ne verrouillez que de haut en bas) et d'espérer que cela n'a pas changé. vous après avoir obtenu la fenêtre racine et que vous verrouillez le haut en bas.
Monstre à cliquet
@ratchetfreak: Si l'enfant que vous tentez de verrouiller est supprimé par un autre thread alors que vous le verrouillez, c'est un peu regrettable, mais sans rapport avec le verrouillage. Vous ne pouvez tout simplement pas opérer sur un objet qu'un autre thread vient de supprimer. Mais pourquoi cet autre thread supprime-t-il les objets / fenêtres que votre thread utilise toujours? Ce n'est pas bon dans n'importe quel scénario, pas seulement l'interface utilisateur.
MSalters
17

La thread (dans un modèle de mémoire partagée) est une propriété qui tend à défier les efforts d'abstraction. Un exemple simple est un Settype: tandis que Contains(..)et Add(...)et Update(...)est une API parfaitement valide dans un scénario à thread unique, le scénario à plusieurs threads nécessite a AddOrUpdate.

La même chose s'applique à l'interface utilisateur - si vous souhaitez afficher une liste d'éléments avec un nombre d'éléments en haut de la liste, vous devez les mettre à jour à chaque modification.

  1. La boîte à outils ne peut pas résoudre ce problème car le verrouillage ne garantit pas que l'ordre des opérations reste correct.
  2. La vue peut résoudre le problème, mais uniquement si vous autorisez la règle de gestion selon laquelle le nombre figurant en haut de la liste doit correspondre au nombre d'éléments de la liste et ne met à jour la liste que dans la vue. Pas exactement ce que MVC est supposé être.
  3. Le présentateur peut le résoudre, mais doit savoir que la vue a des besoins particuliers en matière de threading.
  4. La liaison de données à un modèle multi-threading est une autre option. Mais cela complique le modèle avec des choses qui devraient être des préoccupations d’assurance-chômage.

Aucun de ceux-ci ne semble vraiment tentant. Rendre le présentateur responsable de la gestion des threads est recommandé non pas parce que c'est bon, mais parce que cela fonctionne et que les alternatives sont pires.

Patrick
la source
Peut-être qu'une couche pourrait être introduite entre la vue et le présentateur et pourrait également être remplacée.
durron597
2
.NET a le System.Windows.Threading.Dispatcherpouvoir de gérer la distribution à la fois vers WPF et WinForms UI-Threads. Ce type de couche entre le présentateur et la vue est certainement utile. Mais il ne fournit que l'indépendance de la boîte à outils, pas l'indépendance du thread.
Patrick le
9

Il y a quelque temps, j'ai lu un très bon blog qui traitait de cette question (mentionnée par Karl Bielefeldt). En gros, il est très dangereux d'essayer de sécuriser le thread du kit d'interface utilisateur, car il introduit des impasses et en fonction de mis en œuvre, conditions de course dans le cadre.

Il y a aussi une considération de performance. Pas tellement maintenant, mais lorsque Swing a été publié, il a été vivement critiqué pour ses performances (été médiocre), mais ce n’est pas vraiment sa faute, c’est le manque de connaissances des personnes quant à son utilisation.

SWT applique le concept de sécurité des threads en générant des exceptions si vous le violez, pas beau, mais au moins, vous en êtes conscient.

Si vous regardez dans le processus de peinture, par exemple, l'ordre dans lequel les éléments sont peints est très important. Vous ne voulez pas que la peinture d'un composant ait un effet secondaire sur une autre partie de l'écran. Imaginez que si vous pouviez mettre à jour la propriété text d'une étiquette, mais que celle-ci ait été peinte par deux threads différents, vous pourriez vous retrouver avec une sortie corrompue. Ainsi, toute la peinture est faite dans un seul fil, normalement basé sur l'ordre des exigences / demandes (mais parfois condensé pour réduire le nombre de cycles de peinture physiques réels)

Vous avez parlé de passer de Swing à JavaFX, mais vous auriez ce problème avec à peu près n'importe quel framework d'interface utilisateur (pas seulement les clients lourds, mais également avec le Web). Swing semble être celui qui met en évidence le problème.

Vous pouvez concevoir une couche intermédiaire (contrôleur d’un contrôleur?) Chargée de s’assurer que les appels vers l’UI sont correctement synchronisés. Il est impossible de savoir exactement comment vous pourriez concevoir des parties de votre API non-UI du point de vue de l'API UI et la plupart des développeurs se plaindraient du fait que la protection de thread implémentée dans l'API UI était trop restrictive ou ne répondait pas à leurs besoins. Mieux vaut vous permettre de décider comment vous voulez résoudre ce problème, en fonction de vos besoins

L'un des problèmes les plus importants à prendre en compte est la capacité de justifier un ordre d'événements donné, en fonction d'entrées connues. Par exemple, si l'utilisateur redimensionne la fenêtre, le modèle File d'attente d'événements garantit qu'un ordre donné d'événements se produira. Cela peut sembler simple. Toutefois, si la file d'attente permettait aux événements d'être déclenchés par d'autres threads, vous ne pouvez plus garantir l'ordre les événements peuvent se produire (une situation de concurrence) et tout d'un coup, vous devez commencer à vous soucier de différents états et à ne rien faire jusqu'à ce que quelque chose d'autre se produise et que vous commenciez à devoir partager des drapeaux d'état et que vous vous retrouviez avec des spaghettis.

Ok, vous pouvez résoudre ce problème en ayant une sorte de file d'attente qui ordonne les événements en fonction de l'heure à laquelle ils ont été publiés, mais n'est-ce pas ce que nous avons déjà? En outre, vous ne pouvez toujours pas garantir que le thread B générera ses événements APRES le thread A

La principale raison pour laquelle les gens s'énervent à l'idée de penser à leur code, c'est parce qu'ils sont amenés à penser à leur code / design. "Pourquoi ça ne peut pas être plus simple?" Cela ne peut pas être plus simple, car ce n'est pas un problème simple.

Je me souviens de la sortie de la PS3 et du fait que Sony parlait du processeur Cell et de sa capacité à exécuter des lignes de logique distinctes, à décoder le son, la vidéo, à charger et à résoudre les données du modèle. Un développeur de jeux a demandé: "C'est génial, mais comment synchronisez-vous les flux?"

Le problème dont parlait le développeur était qu’à un moment donné, tous ces flux distincts devaient être synchronisés sur un seul canal de sortie. Le pauvre présentateur haussa simplement les épaules car ce n'était pas un problème qu'ils connaissaient bien. De toute évidence, ils ont eu des solutions pour résoudre ce problème maintenant, mais c'est amusant à l'époque.

Les ordinateurs modernes prennent beaucoup de données simultanément de nombreux endroits différents. Toutes ces données doivent être traitées et transmises à l'utilisateur de manière à ne pas gêner la présentation d'autres informations. Il s'agit donc d'un problème complexe, sans une seule solution simple.

Maintenant, avoir la possibilité de changer de framework, ce n’est pas une chose facile à concevoir, MAIS, prenons MVC un instant, MVC peut être multi-couches, c’est-à-dire que vous pourriez avoir un MVC qui traite directement de la gestion du framework d’interface utilisateur. pourrait alors envelopper le fait que, dans une couche supérieure MVC qui traite des interactions avec d'autres frameworks (potentiellement multi-threadés), il serait de la responsabilité de cette couche de déterminer comment la couche MVC inférieure est notifiée / mise à jour.

Vous utiliseriez ensuite le codage pour interfacer les modèles de conception et les modèles d'usine ou de générateur pour construire ces différentes couches. Cela signifie que vos frameworks multithreads sont découplés de la couche d'interface utilisateur via l'utilisation d'une couche intermédiaire, en tant qu'idée.

MadProgrammer
la source
2
Vous n'auriez pas ce problème avec le Web. JavaScript ne prend délibérément pas en charge les threads - un moteur d'exécution JavaScript est en réalité une file d'attente d'événements volumineuse. (Oui, je sais que JS à proprement parler a WebWorkers - il s’agit de fils traités par nerf, qui se comportent plutôt comme des acteurs dans d’autres langues).
James_pic
1
@James_pic Ce que vous avez réellement, c'est la partie du navigateur qui sert de synchroniseur pour la file d'attente d'événements. C'est essentiellement ce dont nous parlons. L'appelant était responsable de s'assurer que les mises à jour se produisaient avec la file d'attente d'événements du toolkits
MadProgrammer
Oui, exactement. La principale différence, dans le contexte Web, réside dans le fait que cette synchronisation a lieu quel que soit le code de l'appelant, car le moteur d'exécution ne fournit aucun mécanisme permettant l'exécution de code en dehors de la file d'attente des événements. Ainsi, l'appelant n'a pas à en assumer la responsabilité. Je crois que c'est également une partie importante de la motivation derrière le développement de NodeJS: si l'environnement d'exécution est une boucle d'événement, alors tout le code est conscient de la boucle d'événement par défaut.
James_pic
1
Cela dit, il existe des cadres d'interface utilisateur de navigateur qui ont leur propre boucle d'événement dans une boucle d'événement (je vous regarde de manière angulaire). Comme ces infrastructures ne sont plus protégées par le moteur d'exécution, les appelants doivent également s'assurer que le code est exécuté dans la boucle d'événement, comme avec le code multithread dans d'autres infrastructures.
James_pic
Y aurait-il un problème particulier à ce que les commandes "à affichage uniquement" assurent automatiquement la sécurité des threads? Si vous avez un contrôle de texte éditable qui est écrit par un thread et lu par un autre, il n’ya aucun moyen de rendre l’écriture visible pour le thread sans synchroniser au moins l’une de ces actions sur l’état actuel de l’interface utilisateur du contrôle, mais est-ce que de tels problèmes importent pour les contrôles d'affichage uniquement? Je pense que ceux-ci pourraient facilement fournir le verrouillage ou les verrouillages nécessaires pour les opérations effectuées et permettre à l'appelant d'ignorer le moment où ...
Supercat