Je lisais donc une question sur le fait de forcer le ramasse-miettes C # à s'exécuter là où presque toutes les réponses sont identiques: vous pouvez le faire, mais vous ne devriez pas, sauf dans de très rares cas . Malheureusement, personne n’explique ce que sont de tels cas.
Pouvez-vous me dire dans quel type de scénario il est en fait une bonne ou raisonnable idée de forcer le ramassage des ordures?
Je ne demande pas de cas spécifiques C # mais plutôt tous les langages de programmation qui ont un ramasse-miettes. Je sais que vous ne pouvez pas forcer GC sur toutes les langues, comme Java, mais supposons que vous le puissiez.
Réponses:
Vous ne pouvez vraiment pas faire de déclarations générales sur la manière appropriée d'utiliser toutes les implémentations du GC. Ils varient énormément. Je parlerai donc de celui .NET auquel vous avez fait référence à l’origine.
Vous devez connaître le comportement du GC assez intimement pour le faire avec n'importe quelle logique ou raison.
Le seul conseil que je puisse donner sur la collecte est le suivant: ne le faites jamais.
Si vous connaissez vraiment les détails complexes du GC, vous n'aurez pas besoin de mes conseils, cela n'aura donc aucune importance. Si vous ne savez pas déjà avec 100% de confiance, cela vous aidera, et vous devrez chercher en ligne et trouver une réponse comme celle-ci: vous ne devriez pas appeler GC.Collect , ou bien vous devriez aller voir les détails du fonctionnement du GC. à l'intérieur et à l'extérieur, et alors seulement, vous saurez la réponse .
Il y a un endroit sûr où il est logique d'utiliser GC.Collect :
GC.Collect est une API disponible que vous pouvez utiliser pour définir le minutage des événements. Vous pouvez profiler un algorithme, collecter et profiler un autre algorithme immédiatement après, sachant que la GC du premier algorithme ne se produisait pas pendant le second altérant les résultats.
Ce type de profilage est la seule fois que je suggère de collecter manuellement à qui que ce soit.
Exemple contrarié quand même
Un cas d'utilisation possible est que, si vous chargez des objets très volumineux, ils se retrouveront dans le tas d'objets volumineux qui ira directement à la génération 2, bien que la génération 2 soit destinée aux objets à longue durée de vie car elle collecte moins fréquemment. Si vous savez que vous chargez des objets éphémères dans la génération 2 pour une raison quelconque, vous pouvez les nettoyer plus rapidement pour que votre génération 2 reste plus petite et que ses collections soient plus rapides.
C’est le meilleur exemple que je puisse donner, et ce n’est pas bon - la pression de la loi sur la santé que vous établissez ici provoquerait des collectes plus fréquentes, et les collections sont si fréquentes qu’il en est - il est probable que cela effacerait la mémoire au même moment. vite que vous le souffliez avec des objets temporaires. Simplement , je ne fais pas confiance moi - même de présumer une meilleure fréquence de collecte que le GC lui - même - à l' écoute par des gens bien loin plus intelligents que moi
Parlons donc de la sémantique et des mécanismes du GC .NET ... ou ..
Tout ce que je pense savoir sur le .NET GC
S'il vous plaît, toute personne qui trouve des erreurs ici - corrigez-moi. La plupart des membres de GC sont connus pour être de la magie noire et bien que j'essaie de laisser de côté des détails dont je n'étais pas sûr, je me trompe probablement encore.
Vous trouverez ci-dessous de nombreux détails dont je ne suis pas certain, ainsi que des informations beaucoup plus vastes que je ne connais pas. Utilisez ces informations à vos risques et périls.
Concepts GC
Le .NET GC se produit à des moments incohérents. C'est pourquoi on l'appelle "non déterministe". Cela signifie que vous ne pouvez pas vous fier à cela à des moments spécifiques. Il s'agit également d'un ramasse-miettes générationnel, ce qui signifie qu'il partitionne vos objets en fonction du nombre de passes GC qu'ils ont effectuées.
Les objets de la génération 0 ont vécu 0 collections. Celles-ci ont été récemment créées. Aucune collection ne s’est produite depuis leur instanciation. Les objets de votre segment de génération 1 ont traversé une passe de collecte, tout comme les objets de votre segment de génération 2 ont traversé 2 passes de collection.
Maintenant, il convient de noter la raison pour laquelle il qualifie ces générations et partitions en conséquence. Le .NET GC ne reconnaît que ces trois générations, car les passes de collection qui vont sur ces trois tas sont légèrement différentes. Certains objets peuvent survivre des passages de collection des milliers de fois. Le GC ne fait que les laisser de l’autre côté de la partition de pile de la génération 2, il est inutile de les partitionner ailleurs car il s’agit en fait de la génération 44; la collection transmise est la même chose que tout le tas de la génération 2.
Il existe des objectifs sémantiques pour ces générations spécifiques, ainsi que des mécanismes mis en œuvre qui les respectent, et j'y reviendrai dans un instant.
Que contient une collection
Le concept de base d'une passe de collection GC consiste à vérifier chaque objet dans un espace de tas pour voir s'il existe encore des références actives (racines GC) à ces objets. Si une racine GC est trouvée pour un objet, cela signifie que le code en cours d'exécution peut toujours atteindre et utiliser cet objet, de sorte qu'il ne peut pas être supprimé. Toutefois, si aucune racine GC n'est trouvée pour un objet, cela signifie que le processus en cours n'a plus besoin de cet objet. Il peut donc le supprimer pour libérer de la mémoire pour de nouveaux objets.
Maintenant, après avoir fini de nettoyer un tas d'objets et d'en laisser quelques-uns, il y aura un effet secondaire malheureux: des espaces libres entre les objets vivants, là où les objets morts ont été enlevés. Cette fragmentation de la mémoire, si elle est laissée à elle-même, gaspille simplement de la mémoire. Par conséquent, les collections effectuent ce que l'on appelle "compactage": elles prennent tous les objets en direct et les compressent dans le segment de mémoire de sorte que la mémoire disponible soit contiguë d'un côté du segment de mémoire pour Gen. 0.
Maintenant, étant donné l’idée de 3 tas de mémoire, tous partitionnés par le nombre de passes de collecte qu’ils ont effectuées, voyons pourquoi ces partitions existent.
Collection Gen 0
La génération 0 étant les objets les plus récents, elle a tendance à être très petite - vous pouvez donc la collecter en toute sécurité très fréquemment . La fréquence garantit que le segment de mémoire reste petit et les collections sont très rapides car elles sont collectées sur un aussi petit segment. Ceci est basé plus ou moins sur une heuristique affirmant que: Une grande majorité des objets temporaires que vous créez sont très temporaires, ils ne sont donc plus utilisés ou référencés presque immédiatement après leur utilisation, et peuvent donc être collectés.
Collection Gen 1
La génération 1 étant des objets qui ne font pas partie de cette catégorie très temporaire d'objets, leur durée de vie peut encore être assez courte, car une grande partie des objets créés ne sont pas utilisés longtemps. Par conséquent, la génération 1 collecte également assez fréquemment, encore une fois, la taille de son paquet est réduite afin que ses collections soient rapides. Cependant, l’hypothèse est que moins d’objets sont temporaires que la génération 0, donc elle collecte moins souvent que la génération 0
Je dirai franchement que je ne connais pas les mécanismes techniques qui diffèrent entre le laissez-passer de collection du groupe 0 et celui du groupe 1, s’il en existe un autre que la fréquence qu’ils collectent.
Collection Gen 2
Gen 2 doit maintenant être la mère de tous les tas, non? Eh bien oui, c'est plus ou moins vrai. C’est là où vivent tous vos objets permanents - l’objet
Main()
dans lequel vous vivez, par exemple, et tout ce qui estMain()
référencé car ils seront enracinés jusqu’à votreMain()
retour à la fin de votre processus.Étant donné que la génération 2 est un seau pour pratiquement tout ce que les autres générations ne pourraient pas collecter, ses objets sont en grande partie permanents, ou vivent au moins longtemps. Donc, sachant que très peu de ce qui est dans la génération 2 sera en réalité une chose qui peut être collectée, il n’a pas besoin de la collecter fréquemment. Cela permet également à sa collection d'être plus lente, car elle s'exécute beaucoup moins fréquemment. C'est donc essentiellement là où ils ont ajouté tous les comportements supplémentaires pour les scénarios impairs, car ils ont le temps de les exécuter.
Tas d'objets volumineux
Un exemple des comportements supplémentaires de la génération 2 est qu'il effectue également la collecte sur le segment d'objets volumineux. Jusqu'à présent, je parlais entièrement du segment de petits objets, mais le runtime .NET attribue des éléments de certaines tailles à un segment séparé en raison de ce que j'ai appelé le compactage ci-dessus. Le compactage nécessite de déplacer des objets lorsque les collections se terminent sur le segment de petits objets. S'il y a un objet vivant de 10 Mo dans la génération 1, il faudra beaucoup plus de temps pour terminer le compactage après la collecte, ce qui ralentira la collection de la génération 1. Ainsi, cet objet de 10 Mo est affecté au segment d'objets volumineux et collecté au cours de la génération 2, qui s'exécute si rarement.
La finalisation
Un autre exemple est celui des objets avec finaliseurs. Vous placez un finaliseur sur un objet qui fait référence à des ressources dépassant la portée de .NETs GC (ressources non managées). Le finaliseur est le seul moyen dont dispose le GC pour demander la collecte d'une ressource non gérée - vous l'implémentez pour effectuer la collecte / suppression / libération manuelle de la ressource non gérée afin de s'assurer qu'elle ne fuit pas de votre processus. Lorsque le GC commence à exécuter le finaliseur de vos objets, votre implémentation efface la ressource non gérée, ce qui le rend capable de supprimer votre objet sans risquer une fuite de ressource.
Le mécanisme utilisé par les finaliseurs est leur référence directe dans une file d'attente de finalisation. Lorsque le moteur d'exécution attribue un objet à un finaliseur, il ajoute un pointeur à cet objet dans la file d'attente de finalisation et verrouille votre objet à la place (appelé «épinglage») afin que le compactage ne le déplace pas, ce qui romprait la référence à la file d'attente de finalisation. Au fur et à mesure que les collectes se produisent, votre objet n'aura finalement plus de racine GC, mais la finalisation devra être exécutée avant de pouvoir être collectée. Ainsi, lorsque l'objet est mort, la collection déplacera sa référence de la file d'attente de finalisation et y placera une référence sur ce que l'on appelle la file d'attente "FReachable". Ensuite, la collection continue. À un autre moment "non déterministe" de l'avenir, un thread séparé, appelé thread de finaliseur, passera par la file d'attente FReachable, exécutant les finaliseurs pour chacun des objets référencés. Une fois terminé, la file d'attente FReachable est vide et il a basculé un bit sur l'en-tête de chaque objet indiquant qu'il n'est pas nécessaire de le finaliser (ce bit peut également être retourné manuellement avec
GC.SuppressFinalize
ce qui est courant dans lesDispose()
méthodes), je soupçonne également que cela a désépinglé les objets, mais ne me citez pas là-dessus. La prochaine collection qui arrive sur le tas dans lequel se trouve cet objet le récupérera finalement. Les collections de la génération 0 ne font même pas attention aux objets avec ce bit de finalisation nécessaire, elles sont automatiquement promues, sans même vérifier leur racine. Un objet non raciné nécessitant une finalisation dans la génération 1 sera jeté dans laFReachable
file d'attente, mais la collection ne fait rien d'autre avec elle. Elle vit donc dans la génération 2. Ainsi, tous les objets dotés d'un finaliseur neGC.SuppressFinalize
seront collectés dans Gen 2.la source
Je vais donner quelques exemples. Dans l'ensemble, il est rare que forcer un GC soit une bonne idée, mais cela peut en valoir la peine. Cette réponse provient de mon expérience avec la littérature .NET et GC. Il devrait bien se généraliser à d'autres plates-formes (du moins celles qui ont un GC significatif).
Si votre objectif est le débit, plus le GC est rare, mieux ce sera. Dans ces cas, forcer une collection ne peut pas avoir d'impact positif (sauf pour des problèmes plutôt artificiels tels que l'augmentation de l'utilisation du cache du processeur en supprimant les objets morts intercalés dans les objets réels). La collecte par lots est plus efficace pour tous les collectionneurs que je connaisse. Pour une application de production dans la consommation de mémoire à l'état stable, induire un GC n'aide pas.
Les exemples donnés ci-dessus visent la cohérence et la limitation de l'utilisation de la mémoire. Dans ces cas, les GC induits peuvent avoir un sens.
Il semble y avoir une idée répandue selon laquelle le GC est une entité divine qui induit une collection chaque fois que cela est en effet optimal. Je ne connais pas de GC aussi sophistiqué et, en effet, il est très difficile d’être optimal pour le GC. Le GC en sait moins que le développeur. Ses heuristiques sont basées sur des compteurs de mémoire et des choses comme le taux de collecte, etc. Les heuristiques sont généralement bonnes, mais elles ne capturent pas les changements soudains du comportement des applications, tels que la libération de grandes quantités de mémoire gérée. Il est également aveugle aux ressources non gérées et aux exigences de latence.
Notez que les coûts de GC varient en fonction de la taille du segment et du nombre de références sur le segment. Sur un petit tas, le coût peut être très faible. J'ai vu des taux de collecte G2 avec .NET 4.5 de 1 à 2 Go / s sur une application de production de 1 Go.
la source
En règle générale, un ramasse-miettes va collecter lorsqu'il est soumis à une "pression de mémoire" et il est considéré comme une bonne idée de ne pas le collecter à d'autres moments car vous pourriez causer des problèmes de performances ou même des pauses sensibles dans l'exécution de votre programme. Et en fait, le premier point dépend du second: pour un ramasse-miettes générationnel au moins, il fonctionne plus efficacement lorsque le rapport entre les ordures et les objets de qualité augmente. Par conséquent , afin de minimiser le temps passé en pause du programme. , il doit tergiverser et laisser les ordures s’empiler le plus possible.
Le moment approprié pour appeler manuellement le ramasse-miettes est alors, une fois que vous avez fini de faire quelque chose qui 1) est susceptible d'avoir créé beaucoup de poubelle et 2) que l'utilisateur s'attend à prendre un certain temps et à laisser le système inactif. en tous cas. Un exemple classique est à la fin du chargement d’un élément volumineux (un document, un modèle, un nouveau niveau, etc.)
la source
Une chose que personne n’a mentionnée, c’est que, même si le CPG Windows est incroyablement bon, le CPG sur Xbox est un déchet (jeu de mots) .
Ainsi, lorsque vous codez un jeu XNA destiné à être exécuté sur une XBox, il est absolument essentiel de minuter la récupération des ordures au meilleur moment, sinon vous aurez un horrible hoquet intermittent du FPS. En outre, sur XBox, il est courant d’utiliser le
struct
moyen le plus souvent utilisé pour minimiser le nombre d’objets devant être nettoyés.la source
Le ramassage des ordures est avant tout un outil de gestion de la mémoire. En tant que tels, les éboueurs se rassemblent lorsqu'il y a une pression de mémoire.
Les éboueurs modernes sont très bons et s'améliorent. Il est donc peu probable que vous puissiez les améliorer en les ramassant manuellement. Même si vous pouvez améliorer les choses aujourd’hui, il se peut qu’une amélioration future de votre ramasse-miettes choisi rende votre optimisation inefficace, voire contre-productive.
Toutefois , les garbage collectors ne tentent généralement pas d'optimiser l'utilisation de ressources autres que la mémoire. Dans les environnements dépourvus de mémoire, la plupart des ressources hors mémoire précieuses ont une
close
méthode ou une méthode similaire, mais il existe des cas où ce n'est pas le cas pour une raison quelconque, telle que la compatibilité avec une API existante.Dans ces cas, il peut être judicieux d'appeler manuellement la récupération de place lorsque vous savez qu'une ressource non-mémoire valable est utilisée.
RMI
Un exemple concret de ceci est l'invocation de méthode à distance de Java. RMI est une bibliothèque d'appels de procédure distante. Vous avez généralement un serveur, qui met divers objets à la disposition des clients. Si un serveur sait qu'un objet n'est utilisé par aucun client, cet objet est alors éligible pour le garbage collection.
Cependant, le serveur ne le sait que si le client le lui dit et qu'il ne lui dit plus qu'il n'a plus besoin d'un objet une fois que le client a récupéré n'importe quel document.
Cela pose un problème, car le client peut disposer de beaucoup de mémoire libre et ne peut donc pas exécuter une récupération de place très fréquemment. En attendant, le serveur peut avoir beaucoup d'objets inutilisés en mémoire, qu'il ne peut pas collecter car il ne sait pas que le client ne les utilise pas.
Dans RMI, la solution consiste à ce que le client exécute une collecte de place périodiquement, même lorsqu'il dispose de beaucoup de mémoire, pour s'assurer que les objets sont collectés rapidement sur le serveur.
la source
using
bloc ou appeler uneClose
méthode pour s'assurer que la ressource est jetée dès que possible. Compter sur GC pour nettoyer des ressources non-mémoire n’est pas fiable et pose toutes sortes de problèmes (en particulier avec des fichiers dont l'accès doit être verrouillé et qui ne peuvent donc être ouverts qu'une seule fois).close
méthode est disponible (ou que la ressource peut être utilisée avec unusing
bloc), il s'agit de la bonne approche. La réponse traite spécifiquement des rares cas où ces mécanismes ne sont pas disponibles.La meilleure pratique consiste à ne pas forcer une récupération de place dans la plupart des cas. (Tous les systèmes sur lesquels j'ai travaillé avaient des collectes de déchets forcées, des problèmes de soulignement qui, s'ils étaient résolus, auraient éliminé la nécessité de forcer la collecte des déchets et auraient grandement accéléré le système.)
Il existe quelques cas où vous en savez plus sur l'utilisation de la mémoire que le récupérateur de place en connaît. Il est peu probable que cela soit vrai dans une application multi-utilisateur ou dans un service qui répond à plusieurs demandes à la fois.
Cependant, dans certains types de traitement par lots, vous en savez plus que le CPG. Par exemple, considérons une application qui.
Vous pourrez peut- être vérifier (après avoir soigneusement vérifié) que vous devez forcer un nettoyage complet après avoir traité chaque fichier.
Un autre cas est un service qui se réveille toutes les quelques minutes pour traiter certains éléments et ne conserve aucun état pendant son sommeil . Forcer une collection complète juste avant d'aller dormir peut en valoir la peine.
Je préférerais avoir une API de récupération de place lorsque je pourrais lui donner des indices sur ce type de choses sans avoir à forcer un GC moi-même.
Voir aussi " Tidbits de performance de Rico Mariani "
la source
Vous pouvez appeler gc () dans plusieurs cas.
gc()
appel, très peu d'objets restent laissés à eux-mêmes et qu'ils soient déplacés dans un espace de génération plus ancienne ]. Lorsque vous allez créer une grande collection d'objets et utiliser beaucoup de mémoire. Vous voulez simplement libérer autant d’espace que de préparation. Ceci est juste du bon sens. En appelantgc()
manuellement, il n'y aura pas de vérification de graphe de référence redondante sur une partie de cette grande collection d'objets que vous chargez en mémoire. En bref, si vous courezgc()
avant de charger beaucoup en mémoire, legc()
induit pendant la charge se produit au moins une fois au moins lorsque le chargement commence à créer une pression de la mémoire.grosobjets et vous aurez peu de chances de charger plus d’objets en mémoire. En bref, vous passez de la phase de création à la phase d'utilisation. En appelant engc()
fonction de l'implémentation, la mémoire utilisée sera compactée, ce qui améliorera considérablement l'emplacement de la mémoire cache. Cela se traduira par une amélioration massive des performances que vous ne tirerez pas du profilage .gc()
et que l'implémentation de la gestion de la mémoire prend en charge, vous créerez une bien meilleure continuité pour votre mémoire physique. Cela rend encore une fois la nouvelle grande collection d'objets plus continue et compacte, ce qui améliore les performancesla source
By calling gc() depending on implementation, the memory in used will be compacted which massively improves cache locality. This will result in massive improve in performance that you will not get from profiling.
Si vous allouez une tonne d'objets à la suite, il y a de fortes chances qu'ils soient déjà compactés. Si quelque chose, la collecte des ordures peut les déplacer légèrement. Quoi qu'il en soit, l'utilisation de structures de données denses et non aléatoires en mémoire aura un impact plus important. Si vous utilisez une liste chaînée naïve composée d'un élément par nœud, aucune tromperie manuelle du GC ne compensera cela.Un exemple concret:
J'avais une application Web qui utilisait un très grand ensemble de données qui changea rarement et qui devaient être accessibles très rapidement (assez rapidement pour une réponse par frappe via AJAX).
Ici, il est assez évident de charger le graphique correspondant dans la mémoire et d’y accéder depuis la base de données plutôt que dans la base de données, en mettant à jour le graphique lorsque la base de données est modifiée.
Mais, étant très volumineux, une charge naïve aurait nécessité au moins 6 Go de mémoire, car les données allaient s’accroître à l’avenir. (Je n'ai pas les chiffres exacts, une fois qu'il était clair que mon ordinateur de 2 Go essayait de gérer au moins 6 Go, j'avais toutes les mesures nécessaires pour savoir que cela ne marcherait pas).
Heureusement, cet ensemble de données contenait un grand nombre d'objets immuables dans un popsicle identiques; une fois que j'avais déterminé qu'un lot était identique à un autre, je pouvais faire référence à une référence à une autre, ce qui permettait de collecter une grande quantité de données et donc de tout ranger dans moins d'un demi-pouce.
Tout va bien, mais pour cela, il reste encore plus de 6 Go d'objets en l'espace d'une demi-minute pour arriver à cet état. Laissé à lui-même, GC n'a pas fait face; le pic d'activité par rapport au schéma habituel de l'application (beaucoup moins de désallocations par seconde) était trop marqué.
Donc appeler périodiquement
GC.Collect()
pendant ce processus de construction signifiait que tout fonctionnait sans heurts. Bien sûr, je n'ai pas appelé manuellementGC.Collect()
le reste du temps pendant lequel l'application est exécutée.Ce cas concret est un bon exemple des directives concernant le moment où nous devrions utiliser
GC.Collect()
:La plupart du temps, lorsque je pensais avoir un cas où il
GC.Collect()
valait la peine d'être appelé, parce que les points 1 et 2 s'appliquaient, le point 3 suggérait que cela aggraverait les choses ou du moins ne rendrait pas les choses meilleures (et avec peu ou pas d'amélioration penchez-vous plutôt pour ne pas appeler sur appelant comme approche plus susceptible de se révéler meilleure au cours de la durée de vie d’une application).la source
J'ai un usage pour l'élimination des déchets qui est un peu peu orthodoxe.
Il existe cette pratique malavisée qui est malheureusement très répandue dans le monde C #, consistant à mettre au rebut des objets en utilisant l’idiome laid, maladroit, inélégant et sujet aux erreurs, connu sous le nom de «se débarrasser d’Isposable» . MSDN le décrit en détail , et beaucoup de gens jurent, le suivent religieusement, passent des heures et des heures à discuter précisément de la marche à suivre, etc.
(Veuillez noter que ce que j'appelle laid, ce n'est pas le modèle d'élimination d'objet lui-même; ce que j'appelle laid, c'est l'
IDisposable.Dispose( bool disposing )
idiome particulier .)Cet idiome a été inventé car il est censé être impossible de garantir que le destructeur de vos objets sera toujours appelé par le ramasse-miettes pour nettoyer les ressources. Ainsi, les utilisateurs effectuent un nettoyage des ressources à l'intérieur
IDisposable.Dispose()
. S'ils l'oublient, ils le testent également dans le destructeur. Vous savez, juste au cas où.Mais alors vos
IDisposable.Dispose()
objets à nettoyer peuvent être gérés, mais non gérés, mais les objets gérés ne peuvent pas être nettoyés quand ilsIDisposable.Dispose()
sont appelés à partir du destructeur, car ils ont déjà été pris en charge par le ramasse-miettes à ce moment-là. CetteDispose()
méthode nécessite-t-elle une méthode distincte qui accepte unbool disposing
indicateur pour savoir si les objets gérés et non gérés doivent être nettoyés, ou uniquement les objets non gérés?Excusez-moi, mais c'est juste fou.
Je me fie à l'axiome d'Einstein, qui dit que les choses devraient être aussi simples que possible, mais pas plus simples. Il est clair que nous ne pouvons pas omettre le nettoyage des ressources, la solution la plus simple possible doit donc inclure au moins cela. La solution la plus simple suivante consiste à toujours disposer de tout au moment précis où elle est censée être éliminée, sans compliquer les choses en faisant confiance au destructeur comme solution de rechange.
Maintenant, à proprement parler, il est bien sûr impossible de garantir qu'aucun programmeur ne commettra jamais l'erreur d'oublier d'invoquer
IDisposable.Dispose()
, mais ce que nous pouvons faire, c'est utiliser le destructeur pour intercepter cette erreur. C'est très simple, vraiment: tout ce que le destructeur doit faire est de générer une entrée de journal s'il détecte que l'disposed
indicateur de l'objet jetable n'a jamais été définitrue
. Ainsi, l’utilisation du destructeur ne fait pas partie intégrante de notre stratégie d’élimination, mais c’est notre mécanisme d’assurance qualité. Et comme il s’agit d’un test en mode débogage uniquement, nous pouvons placer l’ensemble de notre destructeur à l’intérieur d’un#if DEBUG
bloc afin de ne jamais encourir de pénalité de destruction dans un environnement de production. (L'IDisposable.Dispose( bool disposing )
idiome prescrit queGC.SuppressFinalize()
doit être invoqué précisément pour réduire les frais généraux de la finalisation, mais avec mon mécanisme, il est possible d'éviter complètement ces frais généraux sur l'environnement de production.)Cela revient à l' argument éternel erreur dure vs erreur logicielle: l'
IDisposable.Dispose( bool disposing )
idiome est une approche d'erreur logicielle, et représente une tentative pour permettre au programmeur d'oublier l'invocationDispose()
sans que le système échoue, si possible. L'approche de l'erreur dure dit que le programmeur doit toujours s'assurer queDispose()
cela sera invoqué. La pénalité généralement prescrite par la méthode de la grosse erreur est dans la plupart des cas l’échec de l’assertion, mais dans ce cas particulier, nous faisons une exception et réduisons la pénalité à une simple émission d’une entrée du journal des erreurs.Donc, pour que ce mécanisme fonctionne, la version DEBUG de notre application doit effectuer une élimination complète des déchets avant de quitter, afin de garantir que tous les destructeurs seront invoqués et capturera ainsi tous les
IDisposable
objets que nous avons oublié de supprimer.la source
Now, strictly speaking, it is of course impossible to guarantee that no programmer will ever make the mistake of forgetting to invoke IDisposable.Dispose()
En fait, ce n’est pas le cas, bien que je ne pense pas que C # en soit capable. Ne pas exposer la ressource; à la place, fournissez un DSL pour décrire tout ce que vous ferez avec elle (en gros, un monade), ainsi qu'une fonction qui acquiert la ressource, fait les choses, la libère et renvoie le résultat. L'astuce consiste à utiliser le système de typographie pour garantir que, si quelqu'un passe en contrebande une référence à la ressource, elle ne peut pas être utilisée dans un autre appel de la fonction d'exécution.Dispose(bool disposing)
(qui n'est pas défini sur,IDisposable
c'est qu'il est utilisé pour traiter le nettoyage des objets gérés et non gérés dont l'objet est un champ (ou dont il est responsable), ce qui permet de résoudre le mauvais problème. les objets non gérés dans un objet géré sans autre objet jetable àDispose()
traiter, alors toutes les méthodes en font partie (faites en sorte que le finaliseur effectue le même nettoyage si nécessaire) ou ayez uniquement des objets gérés à mettre au rebut (ne disposez pas de finaliseur du tout) et le besoin debool disposing
disparaître.dispose(disposing)
idiome est terribad, mais je le dis parce que les gens utilisent si souvent cette technique et les finaliseurs quand ils ne disposent que de ressources gérées (l'DbConnection
objet est par exemple géré , il n'est ni invoqué ni commandé, et VOUS DEVEZ SEULEMENT TOUJOURS METTRE EN ŒUVRE UN FINALISATEUR AVEC UN CODE NON GÉRÉ, RAYONNÉ, COM MARSHALLED OU UNSAFE . J'ai expliqué plus haut dans ma réponse à quel point les finaliseurs sont terriblement coûteux. Ne les utilisez pas sauf si vous avez des ressources non gérées dans votre classe.dispose(dispoing)
idiome, mais la vérité est que c'est tellement répandu parce que les gens ont tellement peur des documents GC que quelque chose d'aussi sans rapport que celui cela (ildispose
devrait avoir quelque chose à voir avec GC) mérite qu’ils prennent simplement le médicament prescrit sans même l’enquêter. Bon à vous de l’avoir inspecté, mais vous avez raté le plus grand tout (cela encourage les finaliseurs plus souvent qu’ils ne devraient être)Juste pour parler de manière très théorique et en négligeant des problèmes tels que certaines implémentations du GC ralentissant les choses au cours de leurs cycles de collecte, le scénario le plus important auquel je puisse penser pour forcer la récupération de place est un logiciel critique dans lequel les fuites logiques sont préférables aux crashs de pointeur, par exemple, à des moments inattendus pourrait coûter des vies humaines ou quelque chose de ce genre.
Si vous regardez quelques-uns des plus beaux jeux indépendants écrits avec des langages GC comme les jeux Flash, ils coulent comme des fous, mais ils ne plantent pas. Ils pourraient prendre dix fois plus de mémoire 20 minutes de jeu parce qu’une partie de la base de code du jeu avait oublié de définir une référence sur null ou de la supprimer de la liste et les cadences de prise de vue pourraient commencer à souffrir, mais le jeu fonctionne toujours. Un jeu similaire écrit avec un codage C ou C ++ de mauvaise qualité peut planter du fait de l’accès à des pointeurs en suspens du fait du même type d’erreur de gestion des ressources, mais il ne fuirait pas autant.
Pour les jeux, le crash peut être préférable dans le sens où il peut être rapidement détecté et corrigé, mais dans le cas d'un programme critique, un crash à des moments totalement inattendus pourrait tuer quelqu'un. Je pense donc que les principaux cas de figure seraient des scénarios dans lesquels la sécurité ne serait pas bloquée ou d'autres formes de sécurité sont absolument critiques, et une fuite logique est une chose relativement triviale en comparaison.
Le scénario principal dans lequel je pense qu'il est mauvais de forcer la GC est pour des choses où la fuite logique est en réalité moins préférable qu'un crash. Avec les jeux, par exemple, le crash ne tuera pas nécessairement quelqu'un et il peut être facilement détecté et réparé lors des tests internes, alors qu'une fuite logique peut ne pas être repérée, même après la livraison du produit, sauf si elle est si grave qu'elle rend le jeu injouable en quelques minutes. . Dans certains domaines, un crash facilement reproductible lors des tests est parfois préférable à une fuite que personne ne remarque immédiatement.
Un autre cas auquel je peux penser où il serait préférable de forcer GC à une équipe concerne un programme très éphémère, comme tout ce qui est exécuté à partir de la ligne de commande et qui effectue une tâche puis s’arrête. Dans ce cas, la durée de vie du programme est trop courte pour rendre toute fuite logique non triviale. Les fuites logiques, même pour les grandes ressources, ne deviennent généralement problématiques que des heures ou des minutes après avoir exécuté le logiciel. Il est donc improbable qu'un logiciel conçu pour être exécuté pendant seulement 3 secondes ne présente jamais de problèmes de fuites logiques, ce qui pourrait rendre les choses plus difficiles. plus simple d’écrire de tels programmes éphémères si l’équipe vient d’utiliser GC.
la source