Que recherchez-vous lors du débogage des blocages?


25

Récemment, j'ai travaillé sur des projets qui utilisent fortement le filetage. Je pense que je suis OK pour les concevoir; utiliser autant que possible la conception sans état, verrouiller l'accès à toutes les ressources dont plus d'un thread a besoin, etc. Mon expérience en programmation fonctionnelle m'a énormément aidé.

Cependant, lors de la lecture du code de thread d'autres personnes, je suis confus. Je suis en train de déboguer une impasse en ce moment, et puisque le style de codage et la conception sont différents de mon style personnel, j'ai du mal à voir les conditions d'impasse potentielles.

Que recherchez-vous lors du débogage des blocages?


Je demande ceci ici au lieu de SO parce que je veux des pointeurs plus généraux sur le débogage des blocages, pas une réponse spécifique à mon problème.
Michael K

Les stratégies auxquelles je peux penser sont la journalisation (comme plusieurs autres l'ont souligné), examinant en fait le graphique de blocage de qui attend un verrou verrouillé par qui (voir stackoverflow.com/questions/3483094/… pour certains pointeurs) et des annotations de verrouillage (voir clang.llvm.org/docs/ThreadSafetyAnalysis.html ). Même si ce n'est pas votre code, vous pouvez essayer de convaincre l'auteur d'ajouter des annotations - il trouvera probablement des bogues et les corrigera (y compris éventuellement le vôtre) dans le processus.
Don Hatch

Réponses:


23

Si la situation est un véritable blocage (c'est-à-dire que deux threads détiennent deux verrous différents, mais au moins un thread veut un verrou que l'autre thread détient), vous devez d'abord abandonner toutes les préconceptions de la façon dont les threads ordonnent le verrouillage. N'assume rien. Vous voudrez peut-être supprimer tous les commentaires du code que vous regardez, car ces commentaires peuvent vous faire croire quelque chose qui ne tient pas. Il est difficile de le souligner suffisamment: ne présumez rien.

Après cela, déterminez quels verrous sont maintenus pendant qu'un thread tente de verrouiller autre chose. Si vous le pouvez, assurez-vous qu'un fil se déverrouille dans l'ordre inverse du verrouillage. Encore mieux, assurez-vous qu'un thread ne détient qu'un seul verrou à la fois.

Traitez minutieusement l'exécution d'un thread et examinez tous les événements de verrouillage. À chaque verrou, déterminez si un thread contient d'autres verrous et, dans l'affirmative, dans quelles circonstances un autre thread, effectuant un chemin d'exécution similaire, peut accéder à l'événement de verrouillage considéré.

Il est certainement possible que vous ne trouviez pas le problème avant de manquer de temps ou d'argent.


4
+1 Wow, c'est pessimiste ... n'est-ce pas la vérité, cependant. C'est une donnée que vous ne pouvez pas trouver tous les bugs. Merci pour les suggestions!
Michael K

Bruce, votre caractérisation de «véritable impasse» me surprend. Je pensais qu'un blocage entre deux threads se produit lorsque chacun attend un verrou que l'autre détient. Votre définition semble également inclure le cas où un thread, tout en maintenant un verrou, attend pour acquérir un second verrou actuellement détenu par un thread différent. Cela ne me semble pas être une impasse; est-ce ??
Don Hatch,

@DonHatch - Je l'ai mal formulé. La situation que vous décrivez n'est pas une impasse. J'avais espéré transmettre le désordre de déboguer une situation qui inclut un thread tenant le verrou A, puis en essayant d'acquérir le verrou B, tandis que le thread qui contient le verrou B tente d'acquérir le verrou A. Peut-être. Ou peut-être que la situation est beaucoup plus compliquée. Il vous suffit de garder un esprit très ouvert sur l'ordre d'acquisition des verrous. Examinez toutes les hypothèses. Ne fais confiance à rien.
Bruce Ediger

+1 suggérant de lire attentivement le code et d'examiner toutes les opérations de verrouillage isolément. Il est beaucoup plus facile de regarder un graphique complexe en examinant attentivement un seul nœud que d'essayer de voir le tout à la fois. Combien de fois ai-je trouvé le problème juste en regardant le code et en exécutant différents scénarios dans ma tête.
Newtopian

11
  1. Comme d'autres l'ont dit ... si vous pouvez obtenir des informations utiles pour la journalisation, essayez d'abord car c'est la chose la plus simple à faire.

  2. Identifiez les verrous concernés. Changez tous les mutex / sémaphores qui attendent pour toujours en attentes chronométrées ... quelque chose de ridiculement long comme 5 minutes. Consignez l'erreur lorsqu'elle expire. Cela vous indiquera au moins la direction de l'un des verrous impliqués dans le problème. En fonction de la variabilité du timing, vous pourriez avoir de la chance et trouver les deux verrous après quelques exécutions. Utilisez le code / les conditions d'échec de la fonction pour enregistrer une pseudo trace de pile après que l'attente chronométrée n'a pas identifié comment vous y êtes arrivé en premier lieu. Cela devrait vous aider à identifier le thread impliqué dans le problème.

  3. Une autre chose que vous pourriez essayer est de créer une bibliothèque de wrapper autour de vos services mutex / sémaphore. Suivez quels threads ont chaque mutex et quels threads attendent sur le mutex. Créez un thread de contrôle qui vérifie la durée de blocage des threads. Déclenchez sur une durée raisonnable et videz les informations d'état que vous suivez.

À un moment donné, une simple inspection du code ancien sera nécessaire.


6

La première étape (comme le dit Péter) est la journalisation. Bien que, selon mon expérience, cela soit souvent problématique. Dans un traitement parallèle intensif, cela n'est souvent pas possible. Une fois, j'ai dû déboguer quelque chose de similaire avec un réseau de neurones, qui traitait 100 000 nœuds par seconde. L'erreur ne s'est produite qu'après plusieurs heures et même une seule ligne de sortie a tellement ralenti les choses que cela aurait pris des jours. Si la journalisation est possible, concentrez-vous moins sur les données, mais davantage sur le déroulement du programme, jusqu'à ce que vous sachiez dans quelle partie cela se produit. Juste une simple ligne au début de chaque fonction et si vous pouvez trouver la bonne fonction, divisez-la en petits morceaux.

Une autre option consiste à supprimer des parties du code et des données pour localiser le bogue. Peut-être même écrire un petit programme qui ne prend que certaines classes et n'exécute que les tests les plus élémentaires (toujours dans plusieurs threads bien sûr). Supprimez tout ce qui est lié à l'interface graphique, par exemple toute sortie sur l'état de traitement réel. (J'ai trouvé que l'interface utilisateur était assez souvent la source du bug)

Dans votre code, essayez de suivre le flux logique complet de contrôle entre l'initialisation du verrou et sa libération. Une erreur courante pourrait être de verrouiller au début d'une fonction, de déverrouiller à la fin, mais d'avoir une instruction de retour conditionnel quelque part entre les deux. Des exceptions pourraient également empêcher la publication.


"Des exceptions pourraient empêcher la publication" -> Dommage pour les langages qui n'ont pas de variables de portée: /
Matthieu M.

1
@Matthieu: Avoir des variables de portée et les utiliser correctement peut être deux choses différentes. Et il a demandé d'éventuels problèmes en général, sans mentionner une langue spécifique. C'est donc une chose qui peut influencer le flux de contrôle.
thorsten müller

3

Mes meilleurs amis ont été imprimer / enregistrer des déclarations à des endroits intéressants dans le code. Ceux-ci m'aident généralement à mieux comprendre ce qui se passe réellement à l'intérieur de l'application, sans perturber le timing entre les différents threads, ce qui pourrait empêcher la reproduction du bogue.

Si cela échoue, ma seule méthode restante est de regarder le code et d'essayer de construire un modèle mental des différents threads et interactions, et d'essayer de penser à des façons folles possibles de réaliser ce qui est apparemment arrivé :-) Mais je ne le fais pas me considère comme un tueur d'impasse très expérimenté. J'espère que d'autres seront en mesure de donner de meilleures idées, dont je peux aussi apprendre :-)


1
J'ai débogué quelques verrous morts comme ça aujourd'hui. L'astuce consistait à envelopper pthread_mutex_lock () avec une macro qui affiche la fonction, le numéro de ligne, le nom de fichier et le nom de la variable mutex (en le jetant par jetons) avant et après l'acquisition du verrou. Faites de même pour pthread_mutex_unlock (). Quand j'ai vu que mon fil avait gelé, je n'ai eu qu'à regarder les deux derniers messages, il y avait deux fils qui essayaient de se verrouiller mais sans jamais le finir! Il ne reste plus qu'à ajouter un mécanisme pour basculer cela au moment de l'exécution. :-)
Plumenator

3

Tout d'abord, essayez d'obtenir l'auteur de ce code. Il aura probablement l'idée de ce qu'il avait écrit. même si vous deux ne pouvez pas localiser le problème simplement en parlant, au moins vous pouvez vous asseoir avec lui pour localiser la partie de l'impasse, ce qui sera beaucoup plus rapide que vous comprendre son code sans aide.

À défaut, comme l'a dit Péter Török, l'exploitation forestière est probablement la solution. Pour autant que je sache, le débogueur a fait un mauvais travail sur l'environnement multi-threading. essayez de localiser où se trouve la serrure, obtenez un ensemble des ressources qui attendent et dans quel état la condition de course se produit.


non, la journalisation est votre ennemie ici - lorsque vous vous connectez lentement, vous modifiez le comportement du programme au point où il est facile d'obtenir un programme qui fonctionne parfaitement bien avec la journalisation activée, mais des blocages lorsque la journalisation est désactivée. C'est le même genre de problème que vous auriez lors de l'exécution d'un programme sur un processeur unique plutôt que sur un processeur multicœur.
gbjbaanb

@gbjbaanb, je pense que dire que c'est votre ennemi est beaucoup trop dur. Il serait peut-être juste de dire que c'est votre meilleur ami, qui vous laisse tomber de temps en temps. Je suis d'accord avec plusieurs autres personnes sur cette page qui disent que la journalisation est une bonne première étape à suivre, après l'échec de l'examen du code - souvent (en fait la plupart du temps, selon mon expérience) une simple stratégie de journalisation localisera le problème facilement, et vous avez terminé. Sinon, recourez à d'autres méthodes, mais je ne pense pas que ce soit un bon conseil pour éviter d'essayer ce qui est le plus souvent le meilleur outil pour le travail simplement parce qu'il n'est pas toujours utile.
Don Hatch

0

Cette question m'attire;) Tout d'abord, considérez-vous chanceux car vous avez pu reproduire le problème de manière cohérente à chaque exécution. Si vous recevez la même exception avec la même trace de pile à chaque fois, cela devrait être assez simple. Si ce n'est pas le cas, alors ne faites pas autant confiance à la trace de pile, surveillez simplement l'accès aux objets globaux et ses changements d'état pendant l'exécution.


0

Si vous devez déboguer des blocages, vous êtes déjà en difficulté. En règle générale, utilisez des verrous le plus rapidement possible - ou pas du tout, si possible. Toute situation où vous prenez un verrou puis passez au code non trivial doit être évitée.

Cela dépend bien sûr de votre environnement de programmation, mais vous devriez regarder des choses comme les files d'attente séquentielles qui peuvent vous permettre d'accéder à une ressource à partir d'un seul thread.

Et puis il y a une stratégie ancienne mais infaillible: attribuer un "niveau" à chaque verrou, en commençant au niveau 0. Si vous prenez un verrou de niveau 0, vous n'avez pas le droit à d'autres verrous. Après avoir pris un verrou de niveau 1, vous pouvez prendre un verrou de niveau 0. Après avoir pris un verrou de niveau 10, vous pouvez prendre des verrous de niveau 9 ou inférieur, etc.

Si vous trouvez cela impossible à faire, vous devez corriger votre code car vous rencontrerez des blocages.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.