Quelles leçons avez-vous tirées d'un projet qui a presque / échoué en raison d'un mauvais multithreading? [fermé]

11

Quelles leçons avez-vous tirées d'un projet qui a presque / échoué en raison d'un mauvais multithreading?

Parfois, le cadre impose un certain modèle de filetage qui rend les choses d'un ordre de grandeur plus difficiles à obtenir correctement.

Quant à moi, je n'ai pas encore récupéré du dernier échec et je pense qu'il vaut mieux pour moi de ne pas travailler sur tout ce qui a à voir avec le multithreading dans ce cadre.

J'ai trouvé que j'étais bon dans les problèmes de multithreading qui ont un simple fork / join, et où les données ne voyagent que dans une direction (alors que les signaux peuvent voyager dans une direction circulaire).

Je ne suis pas en mesure de gérer l'interface graphique dans laquelle certains travaux ne peuvent être effectués que sur un thread strictement sérialisé (le "thread principal") et d'autres travaux ne peuvent être effectués que sur n'importe quel thread mais le thread principal (les "threads de travail"), et où les données et les messages doivent voyager dans toutes les directions entre N composants (un graphique entièrement connecté).

Au moment où j'ai quitté ce projet pour un autre, il y avait des problèmes de blocage partout. J'ai entendu que 2-3 mois plus tard, plusieurs autres développeurs ont réussi à résoudre tous les problèmes de blocage, au point qu'il peut être expédié aux clients. Je n'ai jamais réussi à découvrir ce morceau de connaissances manquant qui me manque.

Quelque chose au sujet du projet: le nombre d'ID de message (valeurs entières qui décrivent la signification d'un événement qui peut être envoyé dans la file d'attente de messages d'un autre objet, quel que soit le filetage) atteint plusieurs milliers. Les chaînes uniques (messages utilisateur) se chiffrent également à environ un millier.

Ajoutée

La meilleure analogie que j'ai obtenue d'une autre équipe (sans rapport avec mes projets passés ou présents) était de "mettre les données dans une base de données". ("Base de données" se référant à la centralisation et aux mises à jour atomiques.) Dans une interface graphique qui est fragmentée en plusieurs vues s'exécutant toutes sur le même "thread principal" et tous les travaux lourds non GUI sont effectués dans des threads de travail individuels, les données de l'application doivent être stocké dans une seule plase qui agit comme une base de données et laisser la "base de données" gérer toutes les "mises à jour atomiques" impliquant des dépendances de données non triviales. Toutes les autres parties de l'interface graphique ne gèrent que le dessin d'écran et rien d'autre. Les parties de l'interface utilisateur peuvent mettre en cache des éléments et l'utilisateur ne remarquera pas s'il est périmé d'une fraction de seconde, s'il est correctement conçu. Cette "base de données" est également connue comme "le document" dans l'architecture Document-View. Malheureusement - non, mon application stocke toutes les données dans les vues. Je ne sais pas pourquoi c'était comme ça.

Collègues contributeurs:

(les contributeurs n'ont pas besoin d'utiliser des exemples réels / personnels. Les leçons tirées d'exemples anecdotiques, si vous les jugez crédibles, sont également les bienvenues.)

rwong
la source
Je pense qu'être capable de «penser en fils» est en quelque sorte un talent et moins quelque chose qui peut être appris, faute d'un meilleur libellé. Je connais beaucoup de développeurs qui travaillent avec des systèmes parallèles depuis très longtemps, mais ils s'étranglent si les données doivent aller dans plus d'une direction.
dauphique

Réponses:

13

Ma leçon préférée - très durement gagnée! - est que dans un programme multithread le planificateur est un porc sournois qui vous déteste. Si les choses tournent mal, elles le feront, mais de façon inattendue. Si vous vous trompez, vous poursuivrez des heisenbugs étranges (car toute instrumentation que vous ajoutez modifiera les horaires et vous donnera un modèle d'exécution différent).

La seule façon sensée de résoudre ce problème est de corréler strictement toute la gestion des threads dans un morceau de code aussi petit et correct, ce qui est très conservateur pour garantir que les verrous sont correctement maintenus (et avec un ordre d'acquisition globalement constant également). . La façon la plus simple de le faire est de ne pas partager de mémoire (ou d'autres ressources) entre les threads, sauf pour la messagerie qui doit être asynchrone; qui vous permet d'écrire tout le reste dans un style sans fil. (Bonus: la mise à l'échelle sur plusieurs machines dans un cluster est beaucoup plus facile.)

Associés Donal
la source
+1 pour "ne pas partager de mémoire (ou d'autres ressources) entre les threads, sauf pour la messagerie qui doit être asynchrone;"
Nemanja Trifunovic
1
La seule façon? Qu'en est-il des types de données immuables?
Aaronaught
is that in a multithreaded program the scheduler is a sneaky swine that hates you.- non, il fait exactement ce que vous lui avez dit de faire :)
mattnz
@Aaronaught: Les valeurs globales transmises par référence, même si immuables, nécessitent toujours un GC global et cela réintroduit tout un tas de ressources globales. Il est agréable d'utiliser la gestion de la mémoire par thread, car cela vous permet de vous débarrasser de tout un tas de verrous globaux.
Donal Fellows
Ce n'est pas que vous ne pouvez pas transmettre des valeurs de types non basiques par référence, mais qu'il nécessite des niveaux de verrouillage plus élevés (par exemple, le «propriétaire» détenant une référence jusqu'à ce qu'un message revienne, ce qui est facile à gâcher en maintenance) ou un code complexe dans le moteur de messagerie pour transférer la propriété. Ou vous marshalez tout et démarshal dans l'autre thread, ce qui est beaucoup plus lent (vous devez quand même le faire lorsque vous accédez à un cluster). Il est plus facile de passer à l'action et de ne pas partager de mémoire.
Donal Fellows
6

Voici quelques leçons de base auxquelles je peux penser en ce moment (pas à partir de projets qui échouent mais à partir de vrais problèmes vus sur de vrais projets):

  • Essayez d'éviter tout appel bloquant tout en maintenant une ressource partagée. Le modèle d'interblocage commun est que le thread saisit mutex, effectue un rappel, les blocs de rappel sur le même mutex.
  • Protégez l'accès à toutes les structures de données partagées avec une section mutex / critique (ou utilisez celles sans verrouillage - mais n'inventez pas la vôtre!)
  • Ne présumez pas de l'atomicité - utilisez des API atomiques (par exemple InterlockedIncrement).
  • RTFM concernant la sécurité des threads des bibliothèques, objets ou API que vous utilisez.
  • Profitez des primitives de synchronisation disponibles, par exemple des événements, des sémaphores. (Mais attention lorsque vous les utilisez, vous savez que vous êtes en bon état - j'ai vu de nombreux exemples d'événements signalés dans un mauvais état, de sorte que des événements ou des données peuvent être perdus)
  • Supposons que les threads peuvent s'exécuter simultanément et / ou dans n'importe quel ordre et que le contexte peut basculer entre les threads à tout moment (sauf sous un système d'exploitation qui offre d'autres garanties).
Guy Sirton
la source
6
  • L'ensemble de votre projet GUI ne doit être appelé qu'à partir du thread principal . Fondamentalement, vous ne devez pas mettre un seul (.net) "invoke" dans votre interface graphique. Le multithreading doit être bloqué dans des projets distincts qui gèrent l'accès aux données plus lent.

Nous avons hérité d'une partie où le projet GUI utilise une douzaine de threads. Cela ne donne que des problèmes. Interblocages, problèmes de course, appels GUI croisés ...

Carra
la source
Est-ce que «projet» signifie «assemblage»? Je ne vois pas comment la distribution des classes entre les assemblys pourrait causer des problèmes de thread.
nikie
Dans mon projet c'est en effet un assemblage. Mais le point principal est que tout le code de ces dossiers doit être appelé à partir du thread principal, sans exception.
Carra
Je ne pense pas que cette règle soit généralement applicable. Oui, vous ne devez jamais appeler de code GUI depuis un autre thread. Mais la façon dont vous distribuez les classes aux dossiers / projets / assemblys est une décision indépendante.
nikie
1

Java 5 et versions ultérieures ont des exécuteurs qui sont destinés à faciliter la gestion des programmes de style de jointure de fourche multithread.

Utilisez-les, cela enlèvera beaucoup de douleur.

(et, oui, j'ai appris cela d'un projet :))


la source
1
Pour appliquer cette réponse à d'autres langues - utilisez autant que possible des cadres de traitement parallèle de haute qualité fournis par cette langue. (Cependant, seul le temps nous dira si un framework est vraiment génial et très utilisable.)
rwong
1

J'ai une formation en systèmes embarqués durs en temps réel. Vous ne pouvez pas tester l'absence de problèmes causés par le multithreading. (Vous pouvez parfois confirmer la présence). Le code doit être prouvablement correct. Donc, meilleure pratique autour de toutes les interactions de threads.

  • Règle n ° 1: KISS - Si vous n'avez pas besoin d'un fil, n'en faites pas tourner. Sérialiser autant que possible.
  • Règle n ° 2: Ne cassez pas la n ° 1.
  • # 3 Si vous ne pouvez pas prouver que c'est correct, ce n'est pas le cas.
mattnz
la source
+1 pour la règle 1. Je travaillais sur un projet qui allait initialement bloquer jusqu'à ce qu'un autre thread soit terminé - essentiellement un appel de méthode! Heureusement, nous avons décidé contre cette approche.
Michael K
# 3 FTW. Mieux vaut passer des heures à lutter avec les chronogrammes de verrouillage ou tout ce que vous utilisez pour prouver que c'est bien que des mois se demandent pourquoi il se désagrège parfois.
1

Une analogie d'un cours sur le multithreading que j'ai suivi l'année dernière m'a été très utile. La synchronisation des threads est comme un signal de trafic protégeant une intersection (données) contre l'utilisation simultanée de deux voitures (threads). L'erreur que beaucoup de développeurs font est de tourner les lumières rouges dans la plupart de la ville pour laisser passer une voiture parce qu'ils pensent qu'il est trop difficile ou dangereux de déterminer le signal exact dont ils ont besoin. Cela pourrait bien fonctionner lorsque le trafic est faible, mais entraînera un blocage à mesure que votre application se développera.

C'est quelque chose que je savais déjà en théorie, mais après ce cours, l'analogie est vraiment restée avec moi, et j'ai été étonné de voir combien de fois après cela j'enquêterais sur un problème de thread et trouverais une file d'attente géante, ou interromprait la désactivation partout lors d'une écriture dans une variable seuls deux threads ont été utilisés, ou les mutex ont été maintenus longtemps alors qu'ils pouvaient être refactorisés pour l'éviter complètement.

En d'autres termes, certains des pires problèmes de thread sont causés par une surpuissance essayant d'éviter les problèmes de thread.

Karl Bielefeldt
la source
0

Essayez de recommencer.

Au moins pour moi, ce qui a créé une différence, c'est la pratique. Après avoir effectué plusieurs fois le travail multithread et distribué, vous obtenez juste le coup.

Je pense que le débogage est vraiment ce qui rend les choses difficiles. Je peux déboguer du code multithread en utilisant VS mais je suis vraiment complètement perdu si je dois utiliser gdb. Ma faute, probablement.

Une autre chose qui en apprend davantage sur les structures de données sans verrouillage.

Je pense que cette question peut être vraiment améliorée si vous spécifiez le cadre. Les pools de threads .NET et les travailleurs en arrière-plan sont vraiment différents de QThread, par exemple. Il y a toujours quelques problèmes spécifiques à la plate-forme.

Vitor Py
la source
Je suis intéressé à entendre des histoires de n'importe quel cadre, car je crois qu'il y a des choses à apprendre de chaque cadre, en particulier celles auxquelles je n'ai pas été exposé.
rwong
1
les débogueurs sont largement inutiles dans un environnement multi-thread.
Pemdas
J'ai déjà des traceurs d'exécution multithread qui me disent quel est le problème, mais ne m'aideront pas à le résoudre. Le nœud de mon problème est que "selon la conception actuelle, je ne peux pas passer le message X à l'objet Y de cette manière (séquence); il doit être ajouté à une file d'attente géante et il sera finalement traité; mais à cause de cela , il n'y a aucun moyen que les messages apparaissent à l'utilisateur au bon moment - cela se produira toujours de façon anachronique et rendra l'utilisateur très, très confus. Vous devrez peut-être même ajouter des barres de progression, annuler des boutons ou des messages d'erreur à des endroits qui ne devraient pas '' t les avoir . "
rwong
0

J'ai appris que les rappels des modules de niveau inférieur aux modules de niveau supérieur sont un énorme mal car ils provoquent l'acquisition de verrous dans un ordre opposé.

Sergej Zagursky
la source
les rappels ne sont pas mauvais ... le fait qu'ils fassent autre chose que la rupture du fil est probablement la racine du mal. Je serais hautement suspect de tout rappel qui n'a pas simplement envoyé un jeton vers la file d'attente de messages.
Pemdas
La résolution d'un problème d'optimisation (comme la minimisation de f (x)) est souvent implémentée en fournissant le pointeur vers une fonction f (x) à la procédure d'optimisation, qui la "rappelle" tout en recherchant le minimum. Comment le feriez-vous sans rappel?
quant_dev
1
Pas de downvote, mais les rappels ne sont pas mauvais. Appeler un rappel tout en maintenant un verrou est mauvais. N'appelez rien à l'intérieur d'une serrure lorsque vous ne savez pas si elle pourrait se verrouiller ou attendre. Cela inclut non seulement les rappels mais aussi les fonctions virtuelles, les fonctions API, les fonctions dans d'autres modules ("niveau supérieur" ou "niveau inférieur").
nikie
@nikie: Si un verrou doit être maintenu pendant le rappel, le reste de l'API doit être conçu pour être réentrant (difficile!) ou le fait que vous déteniez un verrou doit être une partie documentée de l'API ( malheureux, mais parfois tout ce que vous pouvez faire).
Donal Fellows
@Donal Fellows: Si un verrou doit être maintenu pendant un rappel, je dirais que vous avez un défaut de conception. S'il n'y a vraiment pas d'autre moyen, alors oui, documentez-le! Tout comme vous documenteriez si le rappel sera appelé dans un thread d'arrière-plan. Cela fait partie de l'interface.
nikie