45 votes

Pourquoi la gestion explicite des threads est-elle une mauvaise chose ?

Sur une question précédente j'ai fait un petit faux pas. Vous voyez, j'avais lu des articles sur les fils et j'avais l'impression que c'était la chose la plus savoureuse depuis la gelée de kiwi.

Imaginez donc ma confusion lorsque je lis des choses comme ça :

[Les lectures sont une très mauvaise chose. Ou, du moins, la gestion explicite des fils est une mauvaise chose.

y

La mise à jour de l'interface utilisateur à travers les threads est généralement un signe que vous abusez des threads.

Puisque je tue un chiot chaque fois que quelque chose m'embrouille, considérez que c'est l'occasion de remettre votre karma à flot...

Comment dois-je utiliser le fil ?

109voto

Eric Lippert Points 300275

L'enthousiasme pour l'apprentissage de l'enfilage est excellent, ne vous méprenez pas. L'enthousiasme pour utiliser beaucoup de fils par contre, est symptomatique de ce que j'appelle la maladie du bonheur des fils.

Les développeurs qui viennent de découvrir la puissance des threads commencent à se poser des questions comme "combien de threads puis-je créer dans un programme ?". C'est un peu comme si un étudiant en anglais se demandait "combien de mots puis-je utiliser dans une phrase ?". Le conseil habituel aux écrivains est de faire des phrases courtes et directes, plutôt que d'essayer d'entasser autant de mots et d'idées que possible dans une seule phrase. Il en va de même pour les threads ; la bonne question n'est pas "combien puis-je en créer ?", mais plutôt "comment puis-je écrire ce programme de manière à ce que le nombre de threads soit le plus petit possible". minimum nécessaire pour faire le travail ?"

Les fils résolvent beaucoup de problèmes, c'est vrai, mais ils introduisent aussi d'énormes problèmes :

  • L'analyse des performances des programmes multithreads est souvent extrêmement difficile et profondément contre-intuitive. J'ai vu des exemples réels dans des programmes fortement multithreadés dans lesquels rendre une fonction plus rapide sans ralentir les autres fonctions ni utiliser plus de mémoire. rend le débit total du système plus petit . Pourquoi ? Parce que les fils sont souvent comme les rues du centre-ville. Imaginez que vous preniez toutes les rues et que vous les rendiez plus courtes comme par magie. sans reprogrammer les feux de circulation . Les embouteillages s'amélioreraient-ils ou empireraient-ils ? Écrire des fonctions plus rapides dans les programmes multithreads pousse les processeurs vers une congestion plus rapide .

Ce que vous voulez, c'est que les fils soient comme des autoroutes inter-États : sans feux de signalisation, très parallèles, se croisant en un petit nombre de points très bien définis et soigneusement étudiés. C'est très difficile à faire. La plupart des programmes fortement multithreadés ressemblent davantage à des noyaux urbains denses avec des feux de signalisation partout.

  • Écrire votre propre gestion personnalisée des threads est incroyablement difficile à réaliser. La raison en est que lorsque vous écrivez un programme monofil ordinaire dans un programme bien conçu, la quantité d'"état global" sur laquelle vous devez raisonner est généralement faible. Idéalement, vous écrivez des objets qui ont des limites bien définies et qui ne se soucient pas du flux de contrôle qui invoque leurs membres. Si vous voulez invoquer un objet dans une boucle, ou un switch, ou autre, allez-y.

Les programmes multithreads avec gestion personnalisée des threads requièrent mondial compréhension de tout qu'un fil va faire qui pourrait éventuellement affecter les données qui sont visibles depuis un autre thread. Vous devez avoir le programme entier dans votre tête, et comprendre les principes de base de la programmation. todo les différentes façons dont deux threads peuvent interagir afin d'obtenir un résultat correct et d'éviter les blocages ou la corruption de données. Il s'agit d'un coût important à payer et d'un risque élevé de bogues.

  • Essentiellement, les threads rendent vos méthodes mentir . Laissez-moi vous donner un exemple. Supposons que vous ayez :

    if (!queue.IsEmpty) queue.RemoveWorkItem().Execute() ;

Ce code est-il correct ? S'il est monofilaire, probablement. S'il est multithread, qu'est-ce qui empêche un autre thread de retirer le dernier élément restant ? après l'appel à IsEmpty est exécuté ? Rien, voilà ce qui se passe. Ce code, qui a l'air très bien localement, est une bombe qui attend d'exploser dans un programme multithread. En fait, ce code est :

 if (queue.WasNotEmptyAtSomePointInThePast) ...

ce qui est évidemment assez inutile.

Supposons donc que vous décidiez de résoudre le problème en verrouillant la file d'attente. Est-ce correct ?

lock(queue) {if (!queue.IsEmpty) queue.RemoveWorkItem().Execute(); }

Ce n'est pas correct non plus, nécessairement. Supposons que l'exécution provoque l'exécution d'un code qui attend une ressource actuellement verrouillée par un autre thread, mais que ce thread attend le verrou de la file d'attente - que se passe-t-il ? Les deux threads attendent éternellement. Mettre un verrou autour d'un morceau de code nécessite que vous sachiez tout que le code pourrait éventuellement faire avec tout ressource partagée, de sorte que vous puissiez déterminer s'il y aura des blocages. Encore une fois, c'est un fardeau extrêmement lourd à porter pour quelqu'un qui écrit ce qui devrait être un code très simple. (La bonne chose à faire ici est probablement d'extraire l'élément de travail dans le verrou et de l'exécuter en dehors du verrou. Mais... et si les éléments sont dans une file d'attente parce qu'ils doivent être exécutés dans un ordre particulier ? Maintenant, ce code est faux aussi parce que d'autres threads peuvent alors exécuter les travaux ultérieurs en premier).

  • C'est encore pire. Les spécifications du langage C# garantissent qu'un programme à un seul thread aura un comportement observable qui correspondra exactement à la spécification du programme. En d'autres termes, si vous avez quelque chose comme "if (M(ref x)) b = 10 ;", vous savez que le code généré se comportera comme si x était accédé par M avant b est écrit. Maintenant, le compilateur, le jitter et le CPU sont tous libres d'optimiser cela. Si l'un d'entre eux peut déterminer que M va être vrai et si nous savons que sur ce thread, la valeur de b n'est pas lue après l'appel à M, alors b peut être assigné avant que x soit accédé. Tout ce qui est garanti est que le programme monofilaire semble fonctionner comme il a été écrit .

Les programmes multithreads font no faire cette garantie. Si vous examinez b et x sur un autre thread pendant que celui-ci est en cours d'exécution, alors vous peut voir b changer avant l'accès à x, si cette optimisation est effectuée. Les lectures et les écritures peuvent logiquement être déplacées en avant et en arrière dans le temps les unes par rapport aux autres dans les programmes monofilaires, et ces déplacements peuvent être observés dans les programmes multi-filaires.

Cela signifie que pour écrire des programmes multithreads dont la logique dépend de l'observation des événements dans le même ordre que celui dans lequel le code est écrit, il faut disposer d'un système de gestion de l'information. détaillé la compréhension du "modèle de mémoire" du langage et du moteur d'exécution. Vous devez savoir précisément quelles garanties sont faites sur la façon dont les accès peuvent se déplacer dans le temps. Et vous ne pouvez pas vous contenter de tester sur votre boîtier x86 en espérant que tout ira bien ; les puces x86 ont des optimisations plutôt conservatrices par rapport à d'autres puces.

Ce n'est qu'un bref aperçu de quelques-uns des problèmes que vous rencontrez lorsque vous écrivez votre propre logique multithread. Il y en a beaucoup d'autres. Alors, quelques conseils :

  • Renseignez-vous sur l'enfilage.
  • N'essayez pas d'écrire votre propre gestion des threads dans le code de production.
  • Utilisez des bibliothèques de plus haut niveau écrites par des experts pour résoudre les problèmes liés aux threads. Si vous avez un tas de tâches à effectuer en arrière-plan et que vous souhaitez les confier à des threads de travail, utilisez un pool de threads plutôt que d'écrire votre propre logique de création de threads. Si vous avez un problème qui peut être résolu par plusieurs processeurs à la fois, utilisez la bibliothèque Task Parallel. Si vous souhaitez initialiser une ressource paresseusement, utilisez la classe d'initialisation paresseuse plutôt que d'essayer d'écrire vous-même du code sans verrou.
  • Évitez les états partagés.
  • Si vous ne pouvez pas éviter le partage de l'état, partagez un état immuable.
  • Si vous devez partager un état mutable, préférez l'utilisation de verrous aux techniques sans verrous.

11voto

MusiGenesis Points 49273

La gestion explicite des threads n'est pas intrinsèquement une mauvaise chose, mais c'est plein de dangers et ne devrait pas être fait sauf si c'est absolument nécessaire.

Dire que les fils sont absolument une bonne chose serait comme dire qu'une hélice est absolument une bonne chose : les hélices fonctionnent très bien sur les avions (lorsque les moteurs à réaction ne sont pas une meilleure alternative), mais ne seraient pas une bonne idée sur une voiture.

8voto

Hans Passant Points 475940

Vous ne pouvez pas apprécier le type de problèmes que le threading peut causer à moins d'avoir débogué une impasse à trois. Ou passé un mois à chasser une condition de course qui ne se produit qu'une fois par jour. Alors, allez-y, sautez à pieds joints et faites toutes les erreurs que vous devez faire pour apprendre à craindre la bête et à ne pas avoir de problèmes.

6voto

sibidiba Points 2879

À moins que vous ne soyez en mesure d'écrire un planificateur de noyau à part entière, vous obtiendrez une gestion explicite des threads. toujours mauvais.

Les threads peuvent être la chose la plus géniale depuis le chocolat chaud, mais la programmation parallèle est incroyablement complexe. Cependant, si vous concevez vos threads pour qu'ils soient indépendants, vous ne pouvez pas vous tirer une balle dans le pied.

En règle générale, si un problème est décomposé en threads, ceux-ci doivent être aussi indépendants que possible, avec des ressources partagées aussi peu nombreuses mais bien définies que possible, avec un concept de gestion aussi minimaliste que possible.

6voto

Dan Tao Points 60518

Je ne peux pas offrir une meilleure réponse que celle qui est déjà là. Mais je peut offrir un exemple concret de code multithreadé que nous avions à mon travail qui a été désastreuse.

Un de mes collègues, comme vous, était très enthousiaste à l'égard des fils lorsqu'il en a entendu parler pour la première fois. Il a donc commencé à y avoir du code comme celui-ci dans tout le programme :

Thread t = new Thread(LongRunningMethod);
t.Start(GetThreadParameters());

En gros, il créait des fils de discussion un peu partout.

Donc finalement un autre Un collègue de travail a découvert cela et l'a dit au développeur responsable : Ne faites pas ça ! La création de threads est coûteuse, vous devriez utiliser le pool de threads, etc. etc. Donc beaucoup d'endroits dans le code qui ressemblaient à l'origine à l'extrait ci-dessus ont commencé à être réécrits comme suit :

ThreadPool.QueueUserWorkItem(LongRunningMethod, GetThreadParameters());

Grosse amélioration, non ? Tout est redevenu normal ?

Eh bien, sauf qu'il y a eu un appel particulier dans ce LongRunningMethod qui pourrait potentiellement bloquer pendant un long moment . Soudain, de temps en temps, nous avons commencé à voir que quelque chose dans notre logiciel debe aurait réagi tout de suite... ça n'a pas été le cas. En fait, il se peut qu'il n'ait pas réagi pendant plusieurs semaines. secondes (clarification : Je travaille pour une société de trading, donc c'était une catastrophe totale).

Ce qui s'est passé, c'est que le pool de threads se remplissait de longs appels bloquants, ce qui entraînait l'apparition d'un autre code qui était supposée de se produire très rapidement en étant mis en file d'attente et en ne fonctionnant que bien plus tard qu'il n'aurait dû.

La morale de cette histoire n'est pas, bien sûr, que la première approche consistant à créer vos propres fils est la bonne chose à faire (elle ne l'est pas). Il s'agit simplement du fait que l'utilisation des threads est difficile et sujette à des erreurs, et que, comme d'autres l'ont déjà dit, vous devriez être très Faites attention lorsque vous les utilisez.

Dans notre situation particulière, de nombreuses erreurs ont été commises :

  1. La création de nouveaux fils en premier lieu était une erreur, car elle était beaucoup plus coûteuse que le développeur ne le pensait.
  2. La mise en file d'attente de toutes les tâches d'arrière-plan sur le pool de threads était une erreur car elle traitait toutes les tâches d'arrière-plan sans distinction et ne tenait pas compte de la possibilité que les appels asynchrones soient effectivement bloqués.
  3. Le fait d'avoir une méthode de blocage longue en soi était le résultat d'une utilisation négligente et très paresseuse de l'outil de gestion de l'environnement de l'utilisateur. lock mot-clé.
  4. On n'a pas suffisamment veillé à s'assurer que le code qui était être exécuté sur des threads d'arrière-plan était thread-safe (ce n'était pas le cas).
  5. On n'a pas suffisamment réfléchi à la question de savoir si le fait de rendre une grande partie du code concerné multithreading valait même la peine d'être fait pour commencer . Dans de nombreux cas, la réponse était non : le multithreading ne faisait qu'introduire de la complexité et des bogues, rendant le code moins compréhensible, y (voici l'argument massue) : ils nuisent aux performances.

Je suis heureux de dire que aujourd'hui nous sommes toujours en vie et notre code est dans un état beaucoup plus sain qu'il ne l'était auparavant. Et nous faire Nous utilisons le multithreading à de nombreux endroits où nous l'avons jugé approprié et où nous avons mesuré les gains de performance (comme la réduction de la latence entre la réception d'un tick de données de marché et la confirmation d'une cotation sortante par la bourse). Mais nous avons appris quelques leçons importantes à la dure. Il est fort probable que si vous travaillez un jour sur un grand système hautement multithreadé, vous le ferez aussi.

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