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.