45 votes

Meilleures pratiques en matière de threads

De nombreux projets sur lesquels je travaille ont des implémentations de threading médiocres et c'est moi qui dois les dépister. Existe-t-il une meilleure façon reconnue de gérer le threading ? Mon code est toujours en attente d'un événement qui ne se produit jamais.

Je pense à un modèle de conception ou quelque chose comme ça.

2 votes

Pouvez-vous être un peu plus précis sur vos problèmes ?

71voto

Jon Skeet Points 692016

(En supposant .NET ; des choses similaires s'appliqueraient pour d'autres plateformes).

Eh bien, il y a lots de choses à considérer. Je vous le conseille :

  • L'immuabilité est idéale pour le multithreading. La programmation fonctionnelle fonctionne bien en simultané, en partie grâce à l'accent mis sur l'immuabilité.
  • Utilisez des verrous lorsque vous accédez à des données partagées mutables, tant en lecture qu'en écriture.
  • N'essayez pas de vous libérer de la serrure, sauf si vous y êtes vraiment obligé. Les verrous sont coûteux, mais constituent rarement le goulot d'étranglement.
  • Monitor.Wait devrait presque siempre faire partie d'une boucle de condition, attendre qu'une condition devienne vraie et attendre à nouveau si elle ne l'est pas.
  • Essayez d'éviter de tenir les serrures plus longtemps que nécessaire.
  • Si vous devez acquérir deux serrures en même temps, documentez soigneusement la commande et assurez-vous de toujours utiliser le même ordre.
  • Documentez la sécurité thread de vos types. La plupart des types Ne le fais pas. Il n'est pas nécessaire qu'ils soient thread-safe, il suffit qu'ils ne soient pas hostiles aux threads (c'est-à-dire que "vous pouvez les utiliser à partir de plusieurs threads, mais ce n'est pas le cas"). votre responsabilité d'enlever les serrures si vous voulez les partager)
  • N'accédez pas à l'interface utilisateur (sauf de manière documentée et sûre pour les threads) à partir d'un thread non lié à l'interface utilisateur. Dans les formulaires Windows, utilisez Control.Invoke/BeginInvoke.

Je pense que je peux en trouver d'autres si cela vous est utile, mais je vais m'arrêter là au cas où cela ne le serait pas.

5 votes

Une très belle liste. Il faudrait peut-être ajouter un point comme "Essayez d'éviter de faire appel à un code externe lorsque vous tenez un verrou".

5 votes

La première chose à faire est de ne pas rendre le système multithreading, sauf si cela est vraiment nécessaire. Le deuxième point devrait être : gardez le threading simple, comme les tâches longues exécutées sur un seul thread d'arrière-plan pour que l'interface utilisateur reste réactive.

0 votes

Je pense que vous avez oublié d'ajouter (je crois que vous l'avez déjà dit ailleurs) : si vous écrivez une application Web (ASP.NET), n'utilisez pas de threading explicite, sauf si vous en avez vraiment besoin. Laissez le serveur gérer le threading des requêtes individuelles pour vous. Sinon, il ne sera pas adapté.

38voto

Daniel Earwicker Points 63298

Apprendre à écrire correctement des programmes multithreads est extrêmement difficile et prend du temps.

La première étape est donc de remplacer l'implémentation par une implémentation qui n'utilise pas du tout de threads multiples.

Ensuite, remettez soigneusement le filtrage si, et seulement si, vous en découvrez un besoin réel, lorsque vous aurez trouvé des moyens sûrs très simples pour le faire. Une implémentation non threadée qui fonctionne de manière fiable est bien meilleure qu'une implémentation threadée cassée.

Lorsque vous êtes prêt à commencer, privilégiez les conceptions qui utilisent des files d'attente à sécurité thread pour transférer les éléments de travail entre les threads et veillez à ce que ces éléments de travail ne soient accessibles qu'à un seul thread à la fois.

Essayez d'éviter de vous contenter de pulvériser lock autour de votre code dans l'espoir qu'il devienne thread-safe. Cela ne fonctionne pas. Finalement, deux chemins de code vont acquérir les mêmes verrous dans un ordre différent, et tout s'arrêtera (une fois toutes les deux semaines, sur le serveur d'un client). Ceci est particulièrement probable si vous combinez des threads avec des événements de déclenchement, et que vous maintenez le verrou pendant que vous déclenchez l'événement - le gestionnaire peut prendre un autre verrou, et maintenant vous avez une paire de verrous maintenus dans un ordre particulier. Que se passe-t-il s'ils sont retirés dans l'ordre inverse dans une autre situation ?

En bref, il s'agit d'un sujet tellement vaste et difficile que je pense qu'il est potentiellement trompeur de donner quelques conseils dans une réponse courte et de dire "Allez-y !". - Je suis sûr que ce n'est pas l'intention des nombreuses personnes érudites qui donnent des réponses ici, mais c'est l'impression que beaucoup retirent des conseils résumés.

Au lieu de cela, acheter ce livre .

En voici un résumé très bien formulé à partir de ce site :

Le multithreading s'accompagne également de inconvénients. Le plus important est qu'il peut conduire à des programmes beaucoup plus complexes programmes. Le fait d'avoir plusieurs threads ne ne crée pas de complexité en soi ; c'est l'interaction entre les threads qui crée la complexité. Ceci s'applique que l'interaction soit intentionnelle ou non intentionnelle, et peut entraîner de longs cycles de développement, ainsi qu'une susceptibilité permanente aux bogues intermittents bogues intermittents et non reproductibles. Pour cette raison, il est préférable de garder une telle interaction dans une conception multithread simple - ou de ne pas utiliser le multithreading du tout du tout - sauf si vous avez un penchant particulier penchant particulier pour la réécriture et le débogage !

Un résumé parfait de Stroustrup :

La manière traditionnelle de traiter la concurrence en laissant un groupe de threads dans un espace d'adressage unique et en utilisant des verrous pour essayer de de faire face aux courses de données et aux problèmes de coordination qui en résultent. est probablement la plus mauvaise possible en termes de correction et de compréhensibles.

0 votes

On dirait un problème courant de downvotes de bons commentaires. Je suppose que quelqu'un a des problèmes de colère.

0 votes

Je n'ai pas remarqué cela en général, mais certainement avec l'enfilage. Je pense que les gens n'aiment pas entendre dire que c'est extrêmement difficile. Mais ça l'est, et la plupart des exemples de code sur le web sont trompeurs.

0 votes

+1. J'ai voté pour le haut. C'est un bon conseil sur la façon de gérer un code threadé instable.

18voto

Matt Davis Points 22019

(Comme Jon Skeet, une grande partie de ceci suppose .NET)

Au risque de paraître argumentatif, des commentaires comme ceux-ci me dérangent :

Apprendre à écrire en mode multithread programmes correctement est extrêmement difficile et prend beaucoup de temps.

Les fils doivent être évités lorsque possible...

Il est pratiquement impossible d'écrire un logiciel qui fait quelque chose d'important sans utiliser les threads d'une manière ou d'une autre. Si vous êtes sous Windows, ouvrez votre gestionnaire de tâches, activez la colonne Thread Count, et vous pouvez probablement compter sur les doigts d'une main le nombre de processus qui utilisent un seul thread. Oui, il ne faut pas simplement utiliser des threads pour le plaisir d'utiliser des threads, ni le faire de manière cavalière, mais franchement, je crois que ces clichés sont utilisés trop souvent.

Si je devais résumer la programmation multithread pour les vrais novices, je dirais ceci :

  • Avant de se lancer, il faut d'abord comprendre que la limite de la classe n'est pas la même que la limite du fil. Par exemple, si une méthode de callback de votre classe est appelée par un autre thread (par exemple, le délégué AsyncCallback de la méthode TcpListener.BeginAcceptTcpClient()), comprenez que le callback exécute sur cet autre fil. Ainsi, même si le callback se produit sur le même objet, vous devez synchroniser l'accès aux membres de l'objet dans la méthode de callback. Les threads et les classes sont orthogonaux ; il est important de comprendre ce point.
  • Identifiez les données qui doivent être partagées entre les threads. Une fois que vous avez défini les données partagées, essayez de les regrouper dans une seule classe si possible.
  • Limitez les endroits où les données partagées peuvent être écrites et lues. Si vous parvenez à réduire le nombre de ces endroits à un seul pour l'écriture et un seul pour la lecture, vous vous rendrez un grand service. Ce n'est pas toujours possible, mais c'est un bon objectif à atteindre.
  • Veillez évidemment à synchroniser l'accès aux données partagées en utilisant la classe Monitor ou le mot-clé lock.
  • Si possible, utilisez un seul objet pour synchroniser vos données partagées, quel que soit le nombre de champs partagés différents. Cela simplifiera les choses. Cependant, cela peut également contraindre les choses de manière excessive, auquel cas, vous pouvez avoir besoin d'un objet de synchronisation pour chaque champ partagé. Et à ce stade, l'utilisation de classes immuables devient très pratique.
  • Si vous avez un thread qui a besoin de signaler un ou plusieurs autres threads, je vous recommande fortement d'utiliser la classe ManualResetEvent pour le faire au lieu d'utiliser des événements/délégués.

Pour résumer, je dirais que le filetage n'est pas difficile, mais qu'il peut être fastidieux. Néanmoins, une application correctement threadée sera plus réactive, et vos utilisateurs vous en seront reconnaissants.

EDIT : Il n'y a rien d'"extrêmement difficile" dans ThreadPool.QueueUserWorkItem(), les délégués asynchrones, les diverses paires de méthodes BeginXXX/EndXXX, etc. en C#. En fait, ces techniques rendent beaucoup Il est plus facile d'accomplir diverses tâches de manière ordonnée. Si vous avez une application GUI qui fait des interactions lourdes avec des bases de données, des sockets ou des E/S, il est pratiquement impossible de rendre le front-end réactif à l'utilisateur sans utiliser les threads en arrière-plan. Les techniques que j'ai mentionnées ci-dessus rendent cela possible et sont très faciles à utiliser. Il est important de comprendre les pièges, bien sûr. Je pense simplement que nous rendons un mauvais service aux programmeurs, en particulier aux plus jeunes, lorsque nous disons que la programmation multithread est "extrêmement difficile" ou que les threads "doivent être évités". De tels commentaires simplifient à l'extrême le problème et exagèrent le mythe, alors que la vérité est que le threading n'a jamais été aussi facile. Il existe des raisons légitimes d'utiliser les threads, et des clichés comme celui-ci me semblent tout simplement contre-productifs.

0 votes

J'aime ça - quelque chose est dit si souvent (parce que c'est vrai), ce qui signifie que nous pouvons l'appeler un "cliché" !

0 votes

Ces choses seraient encore meilleures avec l'isolation de la mémoire (c'est-à-dire pas les threads) pour empêcher les programmeurs d'utiliser accidentellement des données dans le mauvais thread. Une VM devrait en fait être capable de marquer l'état des données comme étant sans accès, en lecture seule, verrouillées par les threads et autres, au moins en mode débogage. Cela serait utile.

6voto

jalf Points 142628

Vous pourriez être intéressé par quelque chose comme CSP ou l'une des autres algèbres théoriques permettant de traiter la concurrence. Il existe des bibliothèques CSP pour la plupart des langages, mais si le langage n'a pas été conçu pour cela, il faut un peu de discipline pour l'utiliser correctement. Mais en fin de compte, chaque type de concurrence/threading se résume à des bases assez simples : Évitez les données mutables partagées, et comprenez exactement quand et pourquoi chaque thread peut être amené à bloquer en attendant un autre thread. (Dans CSP, les données partagées n'existent tout simplement pas. Chaque thread (ou processus dans la terminologie CSP) est uniquement autorisés à communiquer avec d'autres personnes par le biais de canaux bloquants de transmission de messages. Comme il n'y a pas de données partagées, les conditions de course disparaissent. Puisque le passage de messages est bloquant, il devient facile de raisonner sur la synchronisation, et de prouver littéralement qu'aucun blocage ne peut se produire).

Une autre bonne pratique, plus facile à intégrer dans le code existant, consiste à attribuer une priorité ou un niveau à chaque serrure de votre système, et à veiller à ce que les règles suivantes soient suivies de manière cohérente :

  • En tenant une serrure au niveau N, vous vous ne pouvez acquérir que de nouveaux cadenas de niveau inférieur
  • Les verrouillages multiples au même niveau doivent être acquis en même temps, en une seule opération unique, qui tente toujours d'acquérir tous les verrous demandés dans dans le même ordre global (Notez que toute opération ordre cohérent fera l'affaire, mais tout thread qui tente d'acquérir un ou plusieurs plusieurs verrous au niveau N, doit les les acquérir dans le même ordre que n'importe quel qu'un autre thread le ferait n'importe où ailleurs dans le code).

En suivant ces règles, il est tout simplement impossible qu'une impasse se produise. Il ne reste plus qu'à se préoccuper des données partagées mutables.

5voto

Aaron Points 659

J'insiste beaucoup sur le premier point que Jon a posté. Plus vous avez d'états immuables (c'est-à-dire des globales qui sont constantes, etc...), plus votre vie sera facile (c'est-à-dire moins de verrous à gérer, moins de raisonnement à faire sur l'ordre d'entrelacement, etc...).

De plus, souvent, si vous avez de petits objets auxquels vous avez besoin que plusieurs threads aient accès, il est parfois préférable de les copier entre les threads plutôt que d'avoir un global partagé et mutable pour lequel vous devez détenir un verrou pour lire/modifier. C'est un compromis entre votre santé mentale et l'efficacité de la mémoire.

1 votes

Pas seulement pour votre santé mentale, mais souvent aussi pour les performances - les objets partagés nécessitent un verrouillage, ce qui nuit à l'évolutivité. Une alternative à la copie ou à l'immuabilité est de s'efforcer de garantir que l'objet n'est visible que par un seul thread à la fois, ce qui (si cela est correct) permet une mutation sûre et sans verrouillage des éléments de travail.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X