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:
- 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?
- 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.
la source
Réponses:
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.
la source
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.
la source
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
Set
type: tandis queContains(..)
etAdd(...)
etUpdate(...)
est une API parfaitement valide dans un scénario à thread unique, le scénario à plusieurs threads nécessite aAddOrUpdate
.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.
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.
la source
System.Windows.Threading.Dispatcher
pouvoir 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.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.
la source