
Ce livre vous apprendra à maximiser les performances du code managé, idéalement sans sacrifier aucun des avantages de l'environnement .NET, ou dans le pire des cas, en sacrifiant un nombre minimal d'entre eux. Vous apprendrez des méthodes de programmation rationnelles, apprendrez ce qu'il faut éviter et, très probablement, surtout, comment utiliser les outils qui sont librement disponibles afin de mesurer le niveau de performance sans aucune difficulté particulière. Le matériel de formation aura un minimum d'eau - seulement le plus nécessaire. Le livre donne exactement ce que vous devez savoir, il est pertinent et concis, ne contient pas trop. La plupart des chapitres commencent par des informations générales et des informations générales, suivis de conseils spécifiques, présentés comme une recette, et se terminent par une section de mesure et de débogage étape par étape pour une grande variété de scénarios.
En cours de route, Ben Watson plongera dans des composants spécifiques de l'environnement .NET, en particulier le Common Language Runtime (CLR) qui le sous-tend, et verra comment la mémoire de votre machine est gérée, le code est généré, l'exécution multithread est organisée et bien plus encore. . On vous montrera comment l'architecture .NET limite à la fois votre outil logiciel et lui fournit des fonctionnalités supplémentaires et comment le choix des chemins de programmation peut affecter de manière significative les performances globales de l'application. En prime, l'auteur partagera avec vous des histoires de l'expérience de création de systèmes .NET très grands, complexes et hautes performances chez Microsoft au cours des neuf dernières années.
Extrait: choisissez la taille de pool de threads appropriée
Au fil du temps, le pool de threads est configuré indépendamment, mais au tout début, il n'a pas d'historique et il démarrera dans l'état initial. Si votre produit logiciel est exceptionnellement asynchrone et utilise un processeur central de manière significative, il peut souffrir de coûts de lancement initial prohibitifs, en attendant la création et la disponibilité d'encore plus de threads. L'ajustement des paramètres de démarrage contribuera à atteindre un état stable plus rapidement afin qu'à partir du moment où l'application démarre, vous avez à votre disposition un certain nombre de threads prêts à l'emploi:
const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads);
Faites attention ici. Lorsque vous utilisez des objets Task, leur répartition sera basée sur le nombre de threads disponibles pour cela. S'il y en a trop, les objets Tâche peuvent subir une planification excessive, ce qui entraînera au moins une diminution de l'efficacité du processeur central en raison d'un changement de contexte plus fréquent. Si la charge de travail n'est pas si élevée, le pool de threads peut basculer vers l'utilisation d'un algorithme qui peut réduire le nombre de threads, le portant à un nombre inférieur à celui spécifié.
Vous pouvez également définir leur nombre maximal à l'aide de la méthode SetMaxThreads, mais cette technique est soumise à des risques similaires.
Pour connaître le nombre requis de threads, laissez ce paramètre seul et analysez votre application dans un état stable à l'aide des méthodes ThreadPool.GetMaxThreads et ThreadPool.GetMinThreads ou des compteurs de performances qui indiquent le nombre de threads impliqués dans le processus.
N'interrompez pas les flux
Interrompre le travail des threads sans coordination avec le travail des autres threads est une procédure assez dangereuse. Les flux doivent se nettoyer et les appeler la méthode Abort ne leur permet pas de se fermer sans conséquences négatives. Lorsqu'un thread est détruit, certaines parties de l'application sont dans un état non défini. Il serait préférable de planter le programme, mais idéalement, un redémarrage propre est nécessaire.
Pour terminer un thread en toute sécurité, vous devez utiliser une sorte d'état partagé, et la fonction de thread elle-même doit vérifier cet état pour déterminer quand il doit se terminer. La sécurité doit être assurée par la cohérence.
En général, vous devez toujours utiliser les objets Task - aucune API n'est fournie pour interrompre une tâche. Afin de pouvoir terminer de manière cohérente un thread, vous devez, comme indiqué précédemment, utiliser le jeton CancellationToken.
Ne changez pas la priorité des threads
En général, la modification de la priorité des threads est une entreprise extrêmement infructueuse. Sous Windows, la répartition des threads est effectuée en fonction de leur niveau de priorité. Si les threads de haute priorité sont toujours prêts à s'exécuter, les threads de faible priorité seront ignorés et, très rarement, ils auront la chance de s'exécuter. En augmentant la priorité d'un thread, vous dites que son travail doit avoir la priorité sur tous les autres travaux, y compris les autres processus. Ce n'est pas sûr pour un système stable.
Il est préférable de réduire la priorité du thread s'il exécute quelque chose qui peut attendre la fin des tâches de priorité normale. Une bonne raison d'abaisser la priorité d'un thread peut être de découvrir un thread hors de contrôle exécutant une boucle infinie. Il est impossible d'interrompre un thread en toute sécurité, donc la seule façon de retourner un thread donné et des ressources processeur est de redémarrer le processus. Jusqu'à ce qu'il devienne possible de fermer le flux et de le faire proprement, réduire la priorité du flux hors de contrôle sera un moyen raisonnable de minimiser les conséquences. Il convient de noter que même les threads avec une priorité inférieure seront toujours garantis pour fonctionner dans le temps: plus ils sont privés de démarrages, plus leur priorité dynamique sera définie par Windows. Une exception est la priorité inactive THREAD_ - PRIORITY_IDLE, dans laquelle le système d'exploitation planifie uniquement un thread à exécuter lorsqu'il n'a littéralement plus rien à démarrer.
Il peut y avoir des raisons bien justifiées d'augmenter la priorité du flux, par exemple la nécessité de répondre rapidement à des situations rares. Mais utiliser de telles techniques doit être très prudent. La distribution des threads dans Windows est effectuée quels que soient les processus auxquels ils appartiennent, donc un thread de haute priorité de votre processus sera lancé au détriment non seulement de vos autres threads, mais également de tous les threads d'autres applications exécutées sur votre système.
Si un pool de threads est utilisé, toutes les modifications de priorité sont annulées chaque fois qu'un thread revient dans le pool. Si vous continuez à gérer les threads de base lors de l'utilisation de la bibliothèque de tâches parallèles, vous devez garder à l'esprit que plusieurs tâches peuvent être lancées dans le même thread avant qu'il ne soit renvoyé au pool.
Synchronisation et blocage des threads
Dès que la conversation arrive sur plusieurs threads, il devient nécessaire de les synchroniser. La synchronisation consiste à fournir l'accès d'un seul thread à un état partagé, par exemple à un champ de classe. Habituellement, les threads sont synchronisés à l'aide d'objets de synchronisation tels que Monitor, Semaphore, ManualResetEvent, etc. Parfois, ils sont appelés de manière informelle verrous, et le processus de synchronisation dans un thread spécifique est appelé verrou.
L'une des vérités fondamentales des verrous est la suivante: ils n'augmentent jamais les performances. Dans le meilleur des cas - avec une primitive de synchronisation bien implémentée et sans concurrence - le blocage peut être neutre. Cela conduit à arrêter l'exécution de travaux utiles par d'autres threads et au fait que le temps CPU est perdu, augmente le temps de changement de contexte et entraîne d'autres conséquences négatives. Vous devez accepter cela parce que l'exactitude est beaucoup plus importante que la simple performance. Que le résultat incorrect soit rapidement calculé n'a pas d'importance!
Avant de commencer à résoudre le problème de l'utilisation de l'appareil de verrouillage, nous considérerons les principes les plus fondamentaux.
Dois-je vraiment me soucier de la performance?
Justifiez d'abord la nécessité d'augmenter la productivité. Cela nous ramène aux principes abordés au chapitre 1. Les performances ne sont pas tout aussi importantes pour l'ensemble de votre code d'application. Tous les codes ne doivent pas subir d'optimisation au n degré. En règle générale, tout commence par la «boucle intérieure» - le code qui est exécuté le plus souvent ou le plus critique pour la performance - et se propage dans toutes les directions jusqu'à ce que les coûts dépassent les avantages reçus. Il existe de nombreux domaines dans le code qui sont beaucoup moins importants en termes de performances. Dans une telle situation, si vous avez besoin d'un verrou, appliquez-le calmement.
Et maintenant, vous devez faire attention. Si votre morceau de code non critique est exécuté dans un thread à partir d'un pool de threads et que vous le bloquez pendant une longue période, le pool de threads peut commencer à insérer plus de threads pour gérer d'autres demandes. Si un ou deux threads le font de temps en temps, ça va. Mais si beaucoup de threads font de telles choses, un problème peut survenir, car à cause de cela, les ressources qui doivent faire le vrai travail sont dépensées inutilement. Une inadvertance lors du démarrage d'un programme avec une charge constante importante peut avoir un impact négatif sur le système, même pour les parties pour lesquelles les hautes performances sont sans importance, en raison d'un changement de contexte inutile ou d'une implication déraisonnable du pool de threads. Comme dans tous les autres cas, des mesures doivent être prises pour évaluer la situation.
Avez-vous vraiment besoin d'une serrure?
Le mécanisme de verrouillage le plus efficace est celui qui ne l'est pas. Si vous pouvez éliminer complètement le besoin de synchronisation des threads, ce sera le meilleur moyen d'obtenir des performances élevées. C'est un idéal qui n'est pas si facile à réaliser. Cela signifie généralement que vous devez vous assurer qu'il n'y a pas d'état partagé mutable - chaque demande transitant par votre application peut être traitée indépendamment d'une autre demande ou de certaines données centralisées mutables (lecture-écriture). Cette fonctionnalité sera le meilleur scénario pour atteindre des performances élevées.
Et soyez toujours prudent. Avec la restructuration, il est facile d'aller par dessus bord et de transformer le code en un désordre désordonné que personne, y compris vous-même, ne peut comprendre. Vous ne devez pas aller trop loin à moins qu'une productivité élevée ne soit vraiment un facteur critique et ne puisse être atteinte autrement. Transformez le code en asynchrone et indépendant, mais pour qu'il reste clair.
Si plusieurs threads viennent de lire à partir d'une variable (et qu'il n'y a aucune indication d'écriture dans un flux), la synchronisation n'est pas nécessaire. Tous les threads peuvent avoir un accès illimité. Cela s'applique automatiquement aux objets immuables tels que les chaînes ou les valeurs de types immuables, mais peut s'appliquer à tout type d'objets si vous garantissez l'immuabilité de sa valeur lors de la lecture par plusieurs threads.
S'il existe plusieurs threads qui écrivent dans une variable partagée, voyez si l'accès synchronisé peut être éliminé en passant à l'utilisation d'une variable locale. Si vous pouvez créer une copie temporaire pour le travail, le besoin de synchronisation disparaîtra. Ceci est particulièrement important pour les accès synchronisés répétés. De l'accès à la variable partagée, vous devez passer à l'accès à la variable locale après l'accès unique à la variable partagée, comme dans l'exemple simple suivant d'ajout d'éléments à une collection partagée par plusieurs threads.
object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks);
Ce code peut être converti comme suit:
object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks);
Sur ma machine, la deuxième version du code s'exécute plus de deux fois plus vite que la première.
En fin de compte, un état partagé mutable est un ennemi fondamental de la performance. Il nécessite une synchronisation pour la sécurité des données, ce qui dégrade les performances. Si votre conception a au moins la moindre possibilité d'éviter le blocage, alors vous êtes sur le point d'implémenter un système multi-thread idéal.
Ordre de préférence de synchronisation
Pour décider si un type de synchronisation est nécessaire, il faut comprendre que tous n'ont pas les mêmes caractéristiques de performances ou de comportement. Dans la plupart des situations, il vous suffit d'utiliser un verrou, et cela devrait généralement être l'option d'origine. L'utilisation de quelque chose d'autre que le blocage, pour justifier une complexité supplémentaire, nécessite des mesures intensives. En général, nous considérons les mécanismes de synchronisation dans l'ordre suivant.
1. cadenas / moniteur de classe - conserve la simplicité, l'intelligibilité du code et offre un bon équilibre des performances.
2. L'absence totale de synchronisation. Débarrassez-vous des états mutables partagés, restructurez et optimisez. C'est plus difficile, mais s'il réussit, cela fonctionnera mieux que d'appliquer le blocage (sauf lorsque des erreurs sont commises ou que l'architecture est dégradée).
3. Méthodes simples de verrouillage imbriquées - dans certains scénarios, elles peuvent être plus appropriées, mais dès que la situation devient plus compliquée, continuez à utiliser le verrou de verrouillage.
Et enfin, si vous pouvez vraiment prouver les avantages de leur utilisation, utilisez des verrous plus complexes et complexes (gardez à l'esprit: ils se révèlent rarement aussi utiles que vous l'attendez):
- verrous asynchrones (seront abordés plus loin dans ce chapitre);
- tout le monde.
Des circonstances spécifiques peuvent dicter ou entraver l'utilisation de certaines de ces technologies. Par exemple, il est peu probable que la combinaison de plusieurs méthodes interverrouillées surpasse une seule instruction de verrouillage.
»Plus d'informations sur le livre sont disponibles sur
le site Web de l'éditeur»
Contenu»
Extrait25% de réduction sur les colporteurs -
.NETLors du paiement de la version papier du livre, un livre électronique est envoyé par e-mail.