Le guide du Routard du modèle "Dispose"

  Auteur original : Joe Duffy (http://www.bluebytesoftware.com/blog/default.aspx)
  Traducteur: Frédéric De Lène Mirouze (amethyste16@hotmail.com)

Remerciements
C’est quoi ?
Introduction du traducteur
A qui s’adresse cet article ?
Dispose, Finalisation et gestion des ressources 
Aperçu
Le nettoyage implicite
Le nettoyage explicite
Le modèle Dispose
Exemples d’implémentation 
Le problème du versionnage 
Dispose
Conception de classes disposablesTravailler avec des objets IDisposable
Le block using en C# et VB, la sémantique C++
Finalisation
Les finaliseurs C#
Exemples d’utilisation du modèle Dispose
SafeHandler
HandleCollector et stress mémoire
HandleCollector
Add/Remove MemoryPressure

Remerciements

L'article d'origine a été rédigé par Joe Duffy  qui fait partie de l'équipe qui développe la BCL chez Microsoft. Je le remercie de m’avoir autorisé à réaliser cette traduction provenant d’un article publié dans son blog :

http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae

Ce texte est en partie collectif, vous y trouverez donc les interventions de gens comme Jeffrey Richter, Herb Sutter, Brian Grunkemeyer,Chris Brumme, Brad Abrams, Krzysztof Cwalina, Clemens Szyperski.

Vu leur pédigrée on est dans de bonnes mains.

Introduction du traducteur

Je n’aime pas parler le Van Dame surtout si son usage relève d’une sorte de pédanterie pseudo-moderniste. Être dans le vent est une ambition que je laisse volontiers aux feuilles mortes. Mais l’exercice délicat de la traduction est difficile et appelle d’autant plus au compromis que ce n’est pas mon métier.

Par exemple si je ne cherche pas à traduire handle par poignée comme on le rencontre parfois, un design pattern est pour moi clairement un modèle de conception voire un modèle de développement. Je parle de ressources gérées et non pas managées et le verbe anglais dispose se traduit par détruire, ce qui dans ce papier est très précisément son sens d’ailleurs.

Je n’ai pas toutefois de problème à inventer des néologismes tels finaliseur et sa petite famille, du moment que cela s’écrive sans "z" !

J’espère ne pas avoir fait de trop graves contresens, mais n’hésitez pas à me les signaler. Pour reprendre la formule consacrée, je suis le seul responsable des erreurs contenues dans ce texte.

J'ai particulièrement été intéressé par ce document car il s'agit d'un article de fond. Le sujet est certes moins "sexy" que les classes génériques, mais il correspond à ce genre de bagages qui me fascine toujours chez les véritables gourous: la connaissance intime des petits détails.

De temps à autres j’ai ajouté au texte d’origine une « Note du traducteur ». Il s’agira toujours d’une référence bibliographique où l’on peut trouver des définitions et des clarifications sur tel concept dont parle le texte où apparaît la note. Je ne me suis en aucune manière permis d'altérer le texte d'origine.

A qui s’adresse cet article ?

Je pense qu’un développeur débutant ne pourra pas suivre longtemps. Il est indispensable d’avoir un minimum de bagage en .NET ou dans un autre langage géré tel Java ou éventuellement en C++.

Par ailleurs le document n’est pas un cours, mais plutôt une capture de savoir-faire. En particulier il fait de nombreuses références croisées. Il est donc absolument normal qu’une première lecture vous laisse parfois perplexe. C’est le genre de document que l’on doit lire au moins deux fois pour espérer comprendre.

De plus il présuppose un minimum de connaissance sur le sujet et en particulier le fonctionnement du ramasse-miettes. Vous trouverez une bonne introduction dans l’article de Thomas Gil (Tom) sur DNG : http://www.dotnetguru.org/articles/GC/GC.html

Ce guide de développement ne se confine pas à .NET 1.1, mais s’intéresse largement à la version 2.0 pour laquelle les nouveautés sont nombreuses.

Afin de pouvoir héberger la CLR dans SQL Server 2005 sans lui faire perdre son niveau de fiabilité cinq neuf, Microsoft a du en effet développer de nouveaux concepts : SafeHandle, finaliseur critique, zone d’exécution limitée (CER), contrat de fiabilité…

Un bon article d’introduction peut être lu ici:

http://www.microsoft.com/france/msdn/securite/Reliability_default.mspx

Mais aussi dans le chapitre 4 du livre best-seller de Patrick Smacchia :

http://www.dotnetguru.org/modules.php?op=modload&name=News&file=article&sid=687&mode=thread&order=0&thold=0

D’autres informations utiles peuvent être trouvées (en anglais) un peu partout.

Je vais juste citer ce papier de Chris Brumme au sujet de la finalisation :

http://blogs.msdn.com/cbrumme/archive/2004/02/20/77460.aspx

Et un document récapitulatif sur la gestion des ressources trouvé sur le site GotDotNet :

http://www.gotdotnet.com/team/libraries/whitepapers/resourcemanagement/resourcemanagement.aspx

Maintenant place aux experts !

Dispose, Finalisation et gestion des ressources

Note du traducteur :
C’est ici que débute le papier de Joe Duffy.
Pour info, l’état d’une instance d’objet est l’ensemble des valeurs de ses champs de type valeur.

Le ramasse-miettes réalise un travail extraordinaire dans la gestion des allocations mémoire des objets .NET, mais il n’a jamais été conçu pour prendre en charge la mémoire ou les ressources systèmes non gérées.

Il existe une différence considérable entre ces deux mondes au point de nécessiter un pont logiciel dont l’écriture est largement de la responsabilité des développeurs des API.

Le but principal lorsque l’on traite de telles ressource est très simplement de rendre le plus efficace possible leur utilisation. Plus particulièrement lorsque ces ressources sont limitées en quantité. Vous devez vous efforcer de fournir à vos utilisateurs les moyens d’acquérir et libérer les ressources dont ils ont besoin et mettre en place un filet de sécurité capable d’éviter tout risque de fuite de ces ressources.

Heureusement, .NET fournit une couche d’abstraction capable de masquer aux yeux des développeurs les détails relatifs aux ressources non gérées (handle Windows ou GDI, connexion à une base de données, SafeHandle…). Toutefois leur grande variété possible peut nous amener à prendre en charge nous même leur gestion.

Ce document récapitule les meilleures pratiques pour implémenter les mécanismes explicites ou implicites de nettoyage des ressources. On parle souvent de modèle de conception Dispose ou IDisposable car il implique normalement l’interface IDisposable et sa méthode Dispose pour le nettoyage explicite et éventuellement une méthode Finalize pour le nettoyage implicite.

Suivre correctement ce modèle est important si l’on souhaite garantir un nettoyage propre, opportun et standardisé des ressources allouées.

Krzysztof Cwalina:

Beaucoup de gens qui entendent parler du modèle Dispose pour la première fois se plaignent que le ramasse-miettes ne fait pas son boulot. Ils pensent qu’il devrait collecter les ressources et que cela se passe exactement comme on le fait dans le monde non géré. En réalité le ramasse-miettes n’est absolument pas conçu pour prendre en charge les ressources, mais pour prendre en charge la libération de la mémoire. Cela il le fait très bien.

Aperçu

Les types gérés peuvent à l’occasion envelopper des ressources qui non gérées. Dans de tels cas il est préférable d’utiliser la plus petite classe possible qui encadre de telles ressources. Idéalement on ne devrait n’y trouver que le code d’allocation, d’accès primaire et de libération. Il devient alors possible d’écrire une autre classe entièrement gérée afin de fournir un accès plus naturel. Le modèle de conception Dispose permet de limiter les risques et les difficultés qui seront examinés dans les chapitres qui suivent.

On doit toujours fournir une méthode explicite Dispose afin de libérer de façon déterministe les ressources appartenant à l’instance de la classe. Le nettoyage implicite par l’intermédiaire d’une méthode Finalize est également nécessaire lorsque la classe possède de telle ressource. Toutefois, souvent, la classe enveloppante (par exemple SafeHandle) s’en chargera pour vous. Reste alors au développeur la seule tâche d’implémenter le mécanisme de nettoyage explicite.

 Le nettoyage implicite

On devrait toujours fournir un nettoyage implicite pour protéger des ressources avec un SafeHandle. En fait, implémenter des finaliseurs à la main est rarement nécessaire depuis l’introduction du type SafeHandler dans l’environnement .NET 2.0.

Si une sémantique de finalisation supplémentaire est nécessaire, il est possible d’implémenter la méthode surchargée Finalize en utilisant la syntaxe spéciale mise à disposition par chaque langage (~T() en C# ou !T() en C++). L’environnement va alors pouvoir invoquer le finaliseur de façon non déterministe comme étape dans le processus de finalisation du ramasse-miettes afin de donner une dernière chance au système de libérer les ressources qu’il détient.

Non déterministe signifie simplement que la méthode sera invoquée par le ramasse-miettes à un moment indéterminé dans le temps, une fois qu’il n’existera plus de référence active à l’instance d’objet.

L’implémentation correcte d’un finaliseur est suffisamment difficile, nous verrons pourquoi plus loin, pour que vous deviez en étudier la nécessité.

  Le nettoyage explicite

Chaque fois qu’un type possède des ressources (ou des types qui eux même possèdent des ressources, vous devrez fournir à l’utilisateur un moyen de les libérer explicitement. Les développeurs disposent alors de l’option d’initier la libération des ressources lorsque l’objet n’est plus utilisé.

Vous allégez ainsi la tâche du ramasse-miettes ce qui ne peut qu’améliorer les performances et fournit en plus à l’utilisateur un moyen de réclamer les ressources allouées de façon déterministe.

De plus, si la ressource externe est rare ou coûteuse, comme par exemple les handles alloués par l’OS, les performances peuvent s’améliorer et éviter un épuisement de ces ressources.

Un contrôle explicite doit absolument se faire à l’aide de la méthode standard Dispose exposée depuis l’interface IDisposable - en C++, écrivez simplement un destructeur, ~T(), pour le type T, le compilateur génèrera la plomberie Dispose pour vous -.

Tout ceci implique en fait beaucoup plus de choses qu’écrire une méthode Dispose lorsque l’on créé une classe non scellée.

Clemens Szyperski :

Le seul problème est que la prise en charge automatique de la mémoire et des objets rend difficile de garantir que les ressources portées par les objets soient libérées de façon déterministe, c’est à dire rapidement. La confiance dans le ramasse-miettes entraîne les développeurs à penser qu’ils n’ont jamais à s’en soucier, ce qui n’est pas le cas. En fait, tout objet qui implémente IDisposable devrait être mentalement marqué d’un drapeau rouge de façon à ne jamais être autorisé à quitter le code actif sans que l’on appelle sa méthode Dispose. Le filet de sécurité finalisation/Safe handle n’est pas suffisamment puissant pour que laisser les développeurs produire du code sale – fichier qui reste verrouillé pour une durée indéterminée après un « save » ou un « close » sur le document tandis que l’application continue de tourner. L’utilisation prudente des domaines d’application avec leur déchargement rude (qui déclenche les Safe Handle) est parfois un moyen de s’arranger avec cette rigueur.
 

Herb Sutter :

Les finaliseurs sont en fait pire que cela. A part le fait qu’ils se lancent tardivement (ce qui est en effet un sérieux problème pour beaucoup de type de ressource) ; ils sont de plus moins puissants parce que ils ne peuvent réaliser certaines opération permises à un destructeur (ex : un finaliseur ne peut utiliser un autre objet de façon fiable contrairement au destructeur). L’écriture elle même d’un finaliseur est extrêmement difficile.

De plus la collection des objets finalisables par le ramasse-miettes est elle même très coûteuse : chaque objet finalisable – et le graphe potentiellement important d’objets atteignables depuis lui – est promu à la génération suivante du ramasse miette, ce qui accroît le coût de collection de plusieurs ordres de grandeur.

Aujourd’hui, sur la plupart des ramasse-miettes dont celui de .NET, le bon conseil est : lorsque vous devez nettoyer des ressources, le code de nettoyage doit toujours être fournit sous la forme d’un destructeur (Dispose), jamais un finaliseur. Si vous écrivez un finaliseur, il faut également fournir un destructeur en plus et pas à la place de.

 

Brian Grunkemeyer :

Il y a deux concepts différents, mais qui parfois se confondent, concernant la destruction des objets.

  1. La fin de vie d’une ressource (par exemple un handle Win32)
  2. La fin de vie de l’objet conteneur de la ressource (comme une instance de FileStream)

C++ non géré implémente des destructeurs qui sont déclenchés de façon déterministe lorsque l’objet quitte la portée du code actif ou lorsque le développeur appelle une méthode delete sur le pointeur. Ceci met fin à l’existence de la ressource et, au moins dans le cas d’un delete, de l’objet portant cette ressource.

La finalisation par la CLR ne permet que de lancer du code à la fin de vie de l’objet porteur. Se reposer sur la finalisation comme seul moyen de nettoyer les ressources fait que sa durée d’existence devient identique à celle de l’objet. Ceci peut provoquer des problèmes si l’accès à la ressource est exclusif et que celle-ci est en nombre limité. Les performances de l’application vont alors être altérées.

Utilisez donc plutôt Dispose dans le code géré qui est le mécanisme le plus proche d’un destructeur en C++. Vous reléguez ainsi la finalisation au rang de sécurité ultime contre les utilisateurs qui négligent d’appeler Dispose. C’est le mieux compte tenu des restrictions d’utilisation sur les finaliseurs.

Maintenant que vous comprenez bien cela, notez que Finalize et Dispose servent deux objectifs distincts. Malheureusement ils sont implémentés de façon diverses dans différents langages.

C# désigne un finaliseur comme un destructeur, vous forçant ainsi à utiliser la syntaxe ~T(), tandis que Dispose a la forme d’une méthode normale.

En C++, depuis la version .NET 2, Dispose est généré par le code à l’aide de la syntaxe ~T, tandis que le finaliseur réclame la syntaxe !T().

La syntaxe C# est indiscutablement critiquable en ce qu’elle favorise la confusion entre finalisation et destruction. Par conséquent lorsque vous lisez le mot « destructeur » ou bien ~T() dans une discussion au sujet de Dispose ou la durée de vie d’un objet, soyez attentif à l’intention exacte de l’auteur.

En général, on considère qu’une bonne conception de la part d’un consommateur d’un objet qui implémente IDisposable est d’appeler Dispose lorsqu’il n’a plus besoin de l’objet. C’est facile dans un langage tel C# qui offre une syntaxe spécifique avec le block using. C++ qui automatise complètement l’utilisation du bock using.

Le modèle Dispose

Si votre classe n’est pas scellée et doit effectuer un nettoyage des ressources, il est indispensable de suivre précisément le modèle de conception qui suit.

Pour le classes scellées, ce modèle n’a pas besoin d’être suivit, c’est à dire que vous pouvez vous contenter d’implémenter Dispose et Finalize avec la méthode dite simple.

Même dans ce dernier cas, votre code doit toujours adhérer au guide de codage concernant l’implémentation de la logique interne de Dispose ou Finalize.

Ce modèle a été conçu pour garantir un nettoyage fiable et prévisible, empêcher les fuites temporaires de ressources (comme par exemple un appel à Dispose oublié) et, plus important, fournit un modèle standard, non ambiguë, pour les développeurs et les compilateurs. La description qui suit démontre une ligne de conduite sur quand et pourquoi implémenter un finaliseur et pourquoi certains types IDisposable n’en utilise pas. Enfin, nous discuterons des problèmes de versionnage des classes qui nécessitent un nettoyage, en particulier lorsque l’on introduit un nouveau type dans la hiérarchie.

Herb Sutter:

On ne doit pas écrire un finaliseur si on peut l’éviter. En plus de problèmes déjà mentionnés, la présence d’un finaliseur rend l’objet plus coûteux à utiliser, même si le finaliseur n’est jamais appelé.

Le surcoût lors de l’allocation d’un objet finalisable vient du fait que l’on doit l’ajouter également à une liste d’objets finalisables. Ce coût ne peut être évité même si l’objet supprime la finalisation immédiatement dans son constructeur.

En C++, écrivez simplement le destructeur avec la syntaxe ~T() et le compilateur génèrera automatiquement toute la plomberie nécessaire que nous allons décrire plus loin dans ce chapitre. Dans les cas rares où vous souhaitez écrire vous-même un finaliseur avec !T(), le moyen recommandé est de mettre autant de code que possible dans le finaliseur (c’est à dire de code que le finaliseur peut effectivement prendre en charge - par exemple vous ne pouvez, de façon certaine, atteindre d’autres objets dans un finaliseur), placez le reste dans le destructeur et faites en sorte que celui-ci appelle explicitement le finaliseur.

·        Implémentez toujours le modèle Dispose sur un type non scellé qui contient des ressources qui doivent être ou peuvent être explicitement libérées. (par exemple les handles Windows, les ressources non gérées…). Ce modèle produit un moyen standardisé à la disposition des développeurs pour détruire de façon déterministe les ressources possédées par l’objet. Il aide également les sous-classes à libérer les ressources de sa classe de base.

·        Implémentez toujours complètement le modèle Dispose si les sous-classes sont IDisposable quand bien même la classe de base ne possède aucune ressource IDisposable. Cela permettra aux utilisateurs de la classe de base de coder un modèle Dispose dans de bonnes conditions. Un exemple intéressant est la classe System.IO.Stream. Bien qu’il s’agit d’une classe abstraite et ne possède pas à ce titre de ressources, la plupart de ses sous-classes oui. C’est pourquoi elle implémente le modèle Dispose.

Implémentez toujours IDisposable uniquement sur une classe dont l’une des classes de sa hiérarchie parente ne l’a pas déjà fait. La méthode publique Dispose() doit être non virtuelle et consister seulement en deux lignes : un appel à Dispose(true) suivit d’un appel à GC.SuppressFinalize(this).
L’appel à SuppressFinalize ne doit intervenir que si celui à Dispose a réussi, par conséquent on ne doit pas placer cet appel dans un block finally.
Les types qui héritent d’une autre classe qui suivent déjà ce modèle peuvent et doivent réutiliser l’implémentation existante de Dispose().
 

Brad Abrams:

Nous avons beaucoup débattu au sujet de l’ordre relatif des appels dans la méthode publique Dispose(). Par exemple :

public void Dispose(){

      Dispose(true);
      GC.SuppressFinalize(this);

}

Ou bien :

public void Dispose(){

      GC.SuppressFinalize(this);
      Dispose(true);

}

Nous avons finalement opté pour la première solution qui garantit que GC.SuppressFinalize() n’est appelé que si l’appel à Dispose() à réussi.

 

Jeffrey Richter :

J’ai moi-même beaucoup hésité quant à l’ordre de ces deux lignes. Initialement j’aurai plutôt fais le deuxième choix. Je pensais que si Dispose lève une exception, alors il lèvera la même exception lorsque Finalize sera appelé, il n’y a donc aucun bénéfice à générer une deuxième exception.
J’ai depuis changé d’avis. La raison est que Dispose() appelle Dispose(true) qui peut certes lever une exception, mais lorsque Finalize sera appelé plus tard, c’est Dispose(false) qui sera appelé. Il se peut – ou ne se peut pas- qu’un autre chemin de code soit emprunté et qu’alors l’exception ne soit pas levée.

 

Brian Grunkemeyer :

L’ordre est important pour donner au code de finalisation (dans Dispose(false)) une chance de nettoyer les ressources, même si certaines hypothèses faites à des niveaux plus élevés ne puissent plus être faites.

Une méthode Dispose, doit garantir une libération correcte des ressources à la fin de son exécution. Mais si le code devant offrir cette garantie échoue, on se repose sur le finaliseur qui appelle Dispose(false). Cela peut entraîner la corruption et la perte d’une partie des données, mais à ce moment là le problème est inévitable. On s’assure qu’au moins on n’a pas de fuite de ressource.

Le code de finalisation doit déjà s’accommoder même du cas des objets partiellement construits, par conséquent la charge supplémentaire de gérer les objets où Dispose(true) échoue ne devrait pas être très significative.

·        Toujours créer ou surcharger la méthode protégée Dispose(bool) pour prendre en charge toute la logique de nettoyage. Le nettoyage doit entièrement être effectué dans cette méthode – en tenant compte si nécessaire de l’argument de la méthode. Cet argument sera égal à false si la méthode est invoquée depuis le finaliseur.

Jeffrey Richter :

L’idée est que Dispose(bool) sait s’il est appelé dans le cas d’un nettoyage explicite (l’argument vaut true) ou bien par le ramasse-miettes (l’argument vaut false). Cette distinction est utile car la méthode Dispose(bool) sait de façon certaine si elle peut exécuter du code utilisant des références vers des objets externes. Elle sait alors à coup sûr que ces objets n’ont pas déjà été finalisés ou bien disposés.

 

Joe Duffy :

Le commentaire de Jeff peut sembler un peu exagéré. Par exemple, ne peut-on pas accéder de façon fiable à une référence vers un type qui n’est pas finalisable ?

La réponse est, oui, vous le pouvez. Mais uniquement si vous êtes certain qu’il ne s’appuie pas sur des états eux-mêmes finalisables. Cette confiance, doit s’appuyer sur une analyse de relations complexes directes ou indirectes entre objets. Très difficile à faire en pratique sans compter les difficultés de maintenance au fil des différentes versions du code. En conclusion, vous pouvez être sûr à 100% que ce n’est pas une bonne idée.

·        Toujours appeler Dispose(bool) de la classe de base (si elle existe) comme la dernière opération faite dans le code de votre implémentation de Dispose(bool). Il est important de ne pas modifier la valeur de l’argument booléen au cours de votre logique de nettoyage. Vous vous assurez ainsi que la méthode de la classe de base effectuera son nettoyage et que celui ci n’interviendra pas avant que la sous-classe a terminé son travail au cas où elle a besoin des ressources de la méthode de base.

·        Toujours implémenter un finaliseur si votre objet est responsable du contrôle de la durée de vie d’au moins une ressource qui ne dispose pas de son propre finaliseur. Un type comme SafeHandle par exemple dispose de son propre finaliseur.
Dans les autres cas, les développeurs vont souvent négliger l’écriture d’un code garantissant l’exécution d’une logique de nettoyage explicite.
Si votre classe de base a déjà surchargée Finalize conformément à la forme correcte du modèle, vous ne devez pas faire à nouveau cette surcharge car l’appel à votre surcharge virtuelle Dispose(bool) se fera automatiquement.

Lorsque vous implémentez un finaliseur, placez toute la logique de finalisation dans la méthode Dispose(bool). Votre méthode de finalisation ne fera qu’un simple appel virtuel à Dispose(false) et rien d’autre.

Comme on l’a noté plus haut, toute logique de codage incapable de s’exécuter dans un contexte de finalisation, doit être filtrée de sorte à ne pas être appelée si l’argument est à false.

  • Ne jamais ré-implémenter l’interface IDisposable ou bien surcharger Dispose() ou Finalize() si un type de base de la hiérarchie les a déjà défini de façon conforme au modèle de conception Dispose.
    Vous devez juste surcharger Dispose(boolean) et y intégrer votre logique de nettoyage afin d’être certain que les appels vers les méthodes de la classe de base seront effectués. Ré implémenter Finalize se traduit en fait par un appel inutile à Dispose(bool).

Joe Duffy :

Avoir plusieurs finaliseur dans une hiérarchie de classes qui se conforment au modèle peut se traduire en des appels redondants de la mécanique de nettoyage. Une méthode virtuelle de finalisation qui chaîne automatiquement vers base.Finalize() – précisément ce que le compilateur C# met en place par défaut – effectuera N appels virtuels à Dispose(bool), N étant le nombre de finaliseurs dans la hiérarchie. Il en est ainsi parce que Finalize est appelé virtuellement ce qui en retour appelle virtuellement Dispose(bool). Chacun chaînant sur sa classe de base.

Tant que le code implémenté est résistant à des appels multiples, le seul problème ainsi créé est une charge supplémentaire qui pèsera subtilement sur les performances lors du parcours de la chaîne des appels virtuels.

 

Herb Sutter :

Ce qu’explique Joe au sujet de C# ne se produit pas en C++ parce que C++ génère lui-même la plomberie recommandée. Dispose(bool) est alors le seul point de chaînage vers la classe de base.

 

  • Ne jamais créer de variantes autres de Dispose que celles-ci :
    void Dispose()
    void Dispose(bool disposing)


     

Dispose doit être considéré comme un mot réservé de façon à standardiser ce modèle de et éviter les confusions entre développeurs et compilateurs. Certains langages comme C++ peuvent choisir d’implémenter automatiquement ce modèle sur certains types en ajoutant des méthodes comme ~T() ou !T() et encapsulant leur appel dans Dispose(bool).

Exemples d’implémentation

Le modèle simplifié sans finaliseur (C#)

Dans la majorité des cas, vous n’aurez pas besoin d’implémenter de méthode Finalize. Cet exemple démontre le cas le plus simple, par exemple la présence d’un type SafeHandle dont on doit organiser le nettoyage :

using System;

public class SimpleCleanup : IDisposable
{

      // Liste de champs nécessitant un nettoyage

      private SafeHandle handle;
      private bool disposed = false; // Détection d'appels redondants

      public SimpleCleanup()
      {
            this.handle = /* instanciation */;
      }

      protected virtual void Dispose(bool disposing)
      {
            if (!disposed)
            {

                  if (disposing)
                  {
                        if (handle != null)
                        {
                             handle.Dispose();
                        }
                  }

                  disposed = true;
            }
      }

      public void Dispose()
      {
            Dispose(true);
      }

}

Cas avec finaliseur (C#)

Considérez cet exemple comme le prototype de toute implémentation correcte du cas avec finaliseur. L’exemple montre également à quoi doit ressembler une classe qui introduit pour la première fois dans une hiérarchie une logique de nettoyage :

using System;

 public class ComplexCleanupBase : IDisposable
{
      // Liste de champs nécessitant un nettoyage

      private bool disposed = false; // pour détecter les appels redondants

      public ComplexCleanupBase()
      {
            // allocation des ressources
      }

protected void Dispose(bool disposing)
{
          if (!disposed)
          {
                  if (disposing)
                  {
                        // Logique de nettoyage non finalisable
                  }

                  // logique de nettoyage partagée

                  disposed = true;
            }
      }

      ~ComplexCleanupBase()
      {
            Dispose(false);
      }

      public void Dispose()
      {
            Dispose(true);
            GC.SuppressFinalize(this);
      }

}

Cet exemple montre une classe qui étend ComplexeCleanupBase et s’intègre dans le cycle de vie de Dispose et Finalize pour assurer un comportement correct au nettoyage :

public class ComplexCleanupExtender : ComplexCleanupBase
{
      // Des ressources supplémentaires qui nécessitent un nettoyage

      private bool disposed = false; // détection des appels redondants

      public ComplexCleanupExtender() : base()
      {
            // allocation des ressources liées à la nouvelle classe
      }

      protected override void Dispose(bool disposing)
      {
            if (!disposed)
            {
                  if (disposing)
                  {
                        // Logique de nettoyage non finalisable
                  }

                  // logique de nettoyage des nouvelles ressources, partagée

                  disposed = true;
            }

            base.Dispose(disposing);
      }

}

Notez que ComplexCleanupExtender ne ré implémente pas Dispose ni Finalize puisque la classe parent s’en charge déjà. L’implémentation fournie par la classe de base restera toujours valide dans le contexte des classes dérivées.

Exemple avec finaliseur (C++)

public ref class ComplexCleanupBase
{

private:    bool disposed;

public:

    ComplexCleanupBase() : disposed(false)
    {
        // allocation des ressources
    }

    // implémentation implicite de IDisposable

    virtual ~ComplexCleanupBase()    {

        Console::WriteLine("Base::~dtor");
        if (!disposed)  {

            // logique non finalisable

            this->!ComplexCleanupBase();
            disposed = true;
        }

    }

    virtual !ComplexCleanupBase()   {
        Console::WriteLine("Base::!finalizer");

       if (!disposed)    {
            disposed = true;
        }
    }

};

La surcharge du comportement de Dispose est montrée juste après. Notez que le chaînage vers la classe de base s’effectue automatiquement :

public ref class ComplexCleanupExtender : ComplexCleanupBase

{

private:

    // champs supplémentaires nécessitant une logique de nettoyage

    bool disposed;

   public:

  ComplexCleanupExtender() : disposed(false)   {

        // allocation des ressources
    }

    virtual ~ComplexCleanupExtender()    {

Console::WriteLine("Extender::~finalizer");

        if (!disposed)   {

           // logique non finalisable

           this->!ComplexCleanupExtender();
           disposed = true;
        }
    }

    virtual !ComplexCleanupExtender()   {

         Console::WriteLine("Extender::!finalizer");

        if (!disposed)   {
            // logique partagée
            disposed = true;
        }
    }
}
;

Le problème du versionnage

Si vous choisissez d’ajouter ce modèle de conception à une classe existante et non scellée, vous pouvez perturber à votre insu des sous classes déjà existantes. Pour la même raison, changer de contrat de sémantique entre une classe de base et une classe dérivée peut soulever de subtils problèmes de compatibilité. Introduire une notion de « disposabilité » dans une classe de base alors que rien de tel n’existait auparavant est problématique.

De plus l’ajout de nouvelles méthodes à une classe de base peut générer de nouvelles alertes de compilation sur les sous classes existantes. Ce chapitre aborde les points que vous devez soigneusement étudier avant de prendre une décision.

  • Introduire une méthode publique Dispose() ou une méthode protected Dispose(bool) dans une classe de base, lèvera des alertes de compilation dans les sous classes qui auront elle-même implémentées ces méthodes. Le développeur devra donc soit masquer ces nouvelles méthodes à l’aide du modificateur new ou la surcharger afin de clarifier ses intentions.
    Les développeurs qui traitent les alertes comme des erreurs risquent de ne pas pouvoir compiler leur code.
  • Introduire la méthode virtuelle protected Dispose(bool) peut ne pas avoir l’effet attendu si les sous classes ont déjà implémentées Dispose(bool). La sous classe ne se chaînera en effet pas à la méthode homonyme de sa classe de base. On risque alors d’observer des pertes de ressources temporaires et même des finalisations qui ne se font pas si la sous classe appelle GC.SuppressFinalize. Si vous êtes l’auteur de la sous classe, il est évidemment facile de corriger le problème en ajoutant un base.Dispose(bool). Mais pour les types diffusés publiquement, vous ne pouvez contrôler l’usage fait par vos utilisateurs.
  • Si vous sous classez un type IDisposable qui ne suit pas de façon correcte le modèle précédemment présenté, d’autres complications peuvent se produire.
    La table qui suit résume les différentes possibilités pour permettre à la sous classe de s’abonner correctement à la chaîne de contrôle de la durée de vie des ressources.

    Ce tableau explique comment écrire la sous classe de sorte que celles qui les dériveront ultérieurement dispose d’un contrat clair et standard. Les cas considérés incluent ce que l’on doit faire si la classe de base implémente IDisposable publiquement, de façon privée ou pas du tout, que la classe de base dispose ou non d’une méthode Dispose(bool) et/ou si la classe de base a ou non une surcharge de Finalize.

Note du traducteur :

On peut ou non avoir Dispose(), Dispose(bool) ou un finaliseur.

Les trois premières colonnes de cette table forment la liste de toutes les combinaisons possibles.

Les trois colonnes qui suivent montre ce qui doit être alors fait dans chaque cas.

 

Classe de base

Que faire si on dérive ?

Base expose Dispose ?

 

Base expose la méthode virtuelle Dispose(bool) ?

Base a un finaliseur ?

 

IDisposable

 

Virtual Dispose(bool)

Surcharge de Finalize

 

Non

Non

Non

Implémenter un sealed public

Si absente de la classe de base, alors la créer avec la portée protected, autrement la surcharger avec override

Si surchargée, mais pas scellée dans la classe de base, alors re surcharger et sceller.

Non

Non

Oui

Non

Oui

Non

Non

Oui

Oui

Publiquement

Non

Non

Ré implémenter avec un sealed private

Publiquement

Non

Oui

Non publiquement

Non

Non

Non publiquement

Non

Oui

Publiquement

Oui

Non

Si la version de base n’est pas sealed, alors surchargez avec override sealed

Publiquement

Oui

Oui

Non publiquement

Oui

Non

Non publiquement

Oui

Oui

 

Herb Sutter :

Cette table est construite à partir de ce que génère le compilateur C++ automatiquement lorsqu’il détecte une classe de base qui ne suit pas le modèle de conception que nous avons présenté dans ce chapitre.

 

Note du traducteur :

Depuis .NET 2.0, le modificateur sealed s’applique également aux méthodes et aux propriétés virtuelles. Dans ce cas on doit y ajouter le modificateur override.

Dispose

Si un type implémente IDisposable et se conforme à l’un des modèles présentés précédemment, alors le guide de conception suivant s’applique. Notez que cela concerne tout code qui s’exécute à l’intérieur de Dispose(), Dispose(bool) ou tout autre méthode pouvant être appelée par Dispose.

 Conception de classes disposables

  • Toujours implémenter IDisposable sur chaque type ayant un finaliseur. Cela donne à l’utilisateur un moyen d’effectuer un nettoyage déterministe de ces mêmes ressources dont est responsable le finaliseur. Vous pourriez également implémenter Dispose sur des types sans finaliseur, par exemple lorsque vous détruisez transitivement des objets ou en utilisant des objets qui gèrent déjà leurs ressources avec des finaliseurs.

 

Jeffrey Richter :

Cette règle est très importante et ne devrait pas subir d’exception. Sans elle, un utilisateur du type ne peut contrôler correctement les ressources utilisées.

 

Herb Sutter :

Les langages lèvent généralement une alerte dans ce cas. Si vous avez un finaliseur, vous devez avoir un destructeur (Dispose). La seule exception sont les types par valeur car on ne peut avoir ni finaliseur, ni destructeur (la CLR effectue arbitrairement de nombreuses copies bit à bit, il devient alors impossible d’écrire un code de nettoyage correct dans ce cas).

 

  • Toujours concevoir vos méthodes Dispose de sorte à pouvoir être appelées plusieurs fois. La méthode peut parfaitement choisir de ne rien faire après le premier appel. Mais elle ne doit pas générer d’exception.

 

Brian Grunkemeyer :

Une méthode Dispose(bool) peut être appelée plusieurs fois à cause du mécanisme de résurrection (c’est à dire quelqu’un qui a appelé GC.SuppressFinalize sur votre instance), ou parce que un programme vérifiable peut pour diverses raisons appeler plusieurs fois Dispose ou Finalize. Bien que rare et particulier, ceci n’a rien d’illégal.

 

Herb Sutter :

Malheureusement, appeler Dispose(bool) plusieurs fois n’est pas si étrange que cela. C’est ce que fait la chaîne de finalisation générée automatiquement par C#. C’est pourquoi il est important d’implémenter une seule fois Dispose(bool) dans la hiérarchie.

 

  • Toujours détruire transitivement les champs concernés de votre type à l’intérieur d’une  méthode Dispose. On doit appeler Dispose() sur chaque champ dont le cycle de vie dépend de l’objet.
    Par exemple, considérons un cas où notre objet détient un champ privé de type TextReader. Alors on doit appeler la méthode Dispose du TextReader depuis la méthode Dispose du type principal. Si ce code est implémenté dans Dispose(bool), alors l’appel TextReader.Dispose ne doit se faire que si l’argument booléen est égal à true : atteindre d’autres objets durant la finalisation n’est pas conseillé. De plus, si votre objet ne contrôle pas la durée de vie d’un autre objet IDisposable, alors il ne doit pas chercher à appeler sa méthode Dispose puisqu’une autre partie du code peut parfaitement compter sur le fait qu’il soit actif. Négliger cela peut provoquer des bugs très subtils.

 

Herb Sutter :

En C++, vous pouvez simplement suivre la syntaxe naturelle qui consiste à stocker un type dont la durée de vie est contrôlée par votre propre type dans un type par valeur. Le nettoyage transitif sera alors fait automatiquement. Par exemple on écrira :

  ref class R {
   // …

  private:

    TextReader tr; // par valeur, non par référence (^)

  };

Ici R ::~R() va automatiquement appeler tr.~TextReader() suivant la syntaxe usuelle en C++.

 

  • Considérez qu’il est toujours possible qu’un type IDisposable soit mis à null avant d’être détruit. Par exemple :

 

public class CyclicClassA : IDisposable
{

    private TextReader myReader;
    private CyclicClassB cycle;

    public void Dispose()
    {

        if (myReader != null)
        {
            ((IDisposable)myReader).Dispose();
            myReader = null;
        }

        if (cycle != null)
        {
            CyclicClassB b = cycle;
            cycle = null;
            b.Dispose();
        }
    }
}

public class CyclicClassB : IDisposable
{
    private Bitmap bmp;
    private CyclicClassA cycle;

    public void Dispose()
    {
        if (bmp != null)
        {
            bmp.Dispose();
           bmp = null;
        }

        if (cycle != null)
        {
            CyclicClassA a = cycle;
            cycle = null;
            a.Dispose();
        }
    }
}

Dans cet exemple, étant donné a une instance de CyclicClassA et b une instance de CyclicClassB.

Si a.cycle = b et b.cycle = a, la destruction transitive devrait engendrer une boucle infinie. C’est pourquoi l’objet est mis à null pour éviter ce problème.

  • Evitez de lever une exception depuis Dispose à l’exception de situations critiques où le processus qui exécute le code est corrompu. Les utilisateurs s’attendent à ce que Dispose ne lève jamais d’exception. Examinons l’exemple suivant :

void NaiveConsumer()
{

    TextReader tr = new StreamReader(File.OpenRead("foo.txt"));

    try
    {

        // on fait quelque chose ici

    }
    finally
    {
        tr.Dispose();
        // d'autres trucs par là
    }

}

 

Si Dispose pouvait lever une exception, la logique implémentée ensuite dans le bloc finally ne s’exécuterai jamais. Pour éviter ce problème, l’utilisateur serait obligé de placer un autre bloc try/finally dans le bloc finally ! Cela devient vite très compliqué à suivre.

De plus, si l’exécution de Dispose(false) provoque une exception, le processus se terminerait dans le contexte du finaliseur.

  • Toujours rendre l’objet inutilisable après avoir été détruit. Recréer un objet qui a déjà été détruit est souvent difficile, surtout lorsque des destructions transitives se sont produites. Dans de tels cas, invoquer des appels à des méthodes de cet objet doit lever l’exception ObjectDisposedException. S’il est possible de reconstruire un état, soyez alors certain d’avertir l’utilisateur qu’il doit potentiellement re-détruire plusieurs fois l’objet : la première fois, et à chaque fois après la reconstruction.

L’exemple suivant montre un exemple de ce mécanisme. Il est conçu non pas comme un modèle de conception, mais comme un simple exemple :

class Recreatable : IDisposable
{
    private bool disposed = false; // true: l'état de l'objet est à détruit

    public void Dispose()
    {
        Dispose(true);
        this.disposed = true;
        GC.SuppressFinalize(this);
    }

    // implémentation de Dispose(bool)…

    // Chaque fois que vous appelez une méthode qui accède aux ressources de cette classe,

    // vérifiez si son état disposed est à true.

    // dans ce cas, il faut réouvrir la ressource

    public void DoStuff()
    {
        if (disposed)
        {
            ReOpen();
        }

        // fait le boulot
    }

    // Note:
    // le code qui suit n'est pas thread safe.
    // il ne s'agit pas d'un concept, mais d'une démonstration

    public void ReOpen()
    {
        this.disposed = false;

        GC.ReRegisterForFinalization(this); 

        handle = //obtient un nouveau handle

        otherRes = new OtherResource();

    }

}

 

  • Toujours implémenter une méthode Close pour le nettoyage des classes pour lesquelles une telle terminologie est un standard, comme un fichier ou un socket. Dans ce cas, il est recommandé d’implémenter Close exactement de la même manière que Dispose. L’expérience montre en effet que les développeurs ne pensent pas à appeler les deux méthodes, mais l’une ou l’autre. Essayer de déléguer la responsabilité du nettoyage à Dispose, par exemple en ajoutant un appel à Close() depuis Dispose(). Documenter soigneusement les situations qui s’écartent de ce modèle.

Notez qu’un scénario justifiant une déviation est le cas d’un objet qui peut être ouvert et fermé plusieurs fois sans que l’on souhaite recréer une instance. C’est par exemple le cas de System.Data.SqlClient.SqlConnection. Il est possible d’ouvrir et de fermer une connexion vers une base de données plusieurs fois.

  • Annuler les objets gérés volumineux dans la méthode Dispose. C’est rarement nécessaire, mais peut être intéressant si un champ est coûteux à conserver en l’état car cet objet peut éventuellement rester en mémoire fort longtemps. Le simple fait d’appeler Dispose ne signifie pas que les références de l’objet soient libérées. Cela peut arriver par exemple dans le cas d’un conteneur (l’objet que l’on détruit) qui est référencé depuis une variable à longue durée de vie (comme une variable statique) et n’est pas explicitement annulée. L’annuler peut permettre d’abréger son existence en mémoire en le rendant éligible plus tôt lors d’une collection du ramasse-miettes. La définition d’un objet volumineux et coûteux est bien sûr subjective et doit s’appuyer sur des mesures de performance.
  • Eviter de créer des objets IDisposable de type valeur et ne jamais créer de valeur dont dépend la durée de vie d’un objet géré directement. A l’exception des situations pour lesquelles une valeur ne sera jamais copiée (c’est à dire est uniquement locale dans le corps d’une méthode) ou copiée de façon très contrôlée, il est difficile de prédire comment la destruction d’une valeur va interagir avec la sémantique de passage par copie du type valeur. Par exemple une fois qu’une nouvelle copie a été obtenue, une autre copie pourrait accéder aux champs déjà détruits par une ancienne copie.

Travailler avec des objets IDisposable

·        Toujours détruire une instance d’un objet IDisposable lorsque l’on en a plus besoin. Cette règle est partiellement couverte par une précédente qui statue que chaque objet doit transitivement détruire les champs IDisposable qu’il possède, mais aussi prendre en considération les allocations locales d’objets. Vous devez détruire prioritairement les variables locales dont aucune référence ne sort du bloc de code dans lequel elles ont été instanciées. Le moyen le plus simple d’obtenir cela en C# est d’utiliser un block using. En C++ allouez simplement un objet sur la pile.

Soyez attentif, toutefois, à ne pas détruire un objet encore utilisé. Contrairement à la finalisation, il est très facile d’accidentellement nettoyer une ressources sur un objet encore actif.

·        Ne pas masquer les exceptions qui pourraient être levées dans une méthode Dispose. L’invocation d’une exception lors de l’appel à Dispose ne se produit qu’au cours de situations critiques. Dans de tels cas, il ne serait pas très prudent de la capturer et continuer l’exécution.

Le block using en C# et VB, la sémantique C++

C# et VB proposent le block d’instruction using tandis que C++ offre une sémantique d’allocation dans la pile afin de faciliter le travail des développeurs utilisant des objets IDisposable. L’objectif de ces outils est de définir une portée au delà de laquelle l’objet est automatiquement détruit. Cette action est indépendante du fait que le flux de code soit normal ou résulte d’une exception.

En C# et en VB, using est adapté au portées locales où la durée de vie d’un objet s’étend sur un block de code facilement identifiable. Pour des portées plus larges, comme par exemple les champs IDisposable, il est préférable d’invoquer directement Dispose. En C++, déclarer un champ par valeur est encore la meilleure solution. Le destructeur du champ sera alors appelé automatiquement lorsque l’objet conteneur sera détruit  que ce soit dans le cours normal de l’exécution ou à la suite d’une exception.

L’exemple suivant est en C# :

void UseDisposableObject()
{
    using (Resource r = new Resource())
    {
        // utilise la ressource
    }

}

Ce qui s’écrirait en C++ :

void UseDisposableObject()

{

    Resource r;

 

    // utilise la ressource

}

Le code IL est l’équivalent du code suivant :

void UseDisposableObject()
{
      Resource resource = new Resource();

      try
      {
          // utiliser les ressources
      }
      finally
      {
          if (resource != null)
          {
              ((IDisposable)resource).Dispose();
          }
      }

}

Observez comment l’instruction using organise le nettoyage du code ce qui rend l’implémentation de Dispose plus attrayante pour les développeurs. Il est également possible d’empiler les instructions using comme par exemple :

using (Resource r1 = new Resource())
using
(Resource r2 = new Resource())
{
    // utilise r1 et r2

}

Et en C++ :

{

    Resource r1;

    Resource r2;

 

    // utilise r1 et r2

}

 

Note du traducteur :

L’instruction using est une des nouveautés de VB.NET.

La syntaxe est la suivante :

 

Using resource As New resourceType

    ' utilise la ressource

End Using

 

Finalisation

Si vous implémentez une logique de finalisation, il est important de le faire correctement. Les finaliseurs sont notoirement difficiles à implémenter essentiellement parce que l’on ne peut faire certaines hypothèses (normalement valides) au sujet de l’état du système. Le guide de développement qui suit vous aidera dans cette tâche.

Ce guide ne concerne pas seulement la méthode Finalize (C# : ~T(), C++ : !T()), mais à tout code qui s’exécute durant la finalisation. Dans le cadre du modèle Dispose défini précédemment, il s’agit du code exécuté dans Dispose(bool) lorsque l’argument vaut false.

·        Analysez avec soin tous les cas où vous pensez avoir besoin d’un finaliseur. Il existe un coût réel associé à une instance d’un objet exposant un finaliseur, tant du point de vue des performances que de la complexité du code. Préférez l’utilisation de conteneurs tels SafeHandle chaque fois que possible. Dans ce cas le finaliseur est inutile car le conteneur est responsable de la libération de ses ressources.

La finalisation augmente le coût et la durée de vie de chaque objet finalisable car ils doivent être enregistrés dans une pile spécialisée lors de leur allocation. Essentiellement un pointeur supplémentaire est créé vers cet objet.

De plus, les objets dans la file sont parcourus durant la collection par le ramasse-miettes, traité et éventuellement promus vers une autre file utilisé par le ramasse-miettes afin d’invoquer le finaliseur. L’augmentation du nombre d’objets finalisables accroît mécaniquement le nombre d’objet promus vers les générations supérieures et augmente donc le temps passé par le ramasse-miettes à parcourir cette file, déplacer les pointeurs et lancer le code de finalisation. De plus, en conservant des objets plus longtemps, on augmente le temps durant lequel ils mobilisent de la mémoire.

·        Ecrivez des méthodes Finalize protected, ni public ni private. Les développeurs C# et C++ n’ont en fait pas à s’en soucier car le compilateur le fera pour eux automatiquement.

·        La méthode Finalize ne doit libérer que les ressources non gérées détenue par la classe conteneur. Ne touchez aucun objet finalisable dont votre objet pourrait avoir une référence car vous ne savez pas s’il a déjà été finalisé. Même les objets gérés dont vous contrôlez le cycle de vie peuvent être dans un état indéfini.

Par exemple, un objet finalisable A possédant une référence vers un autre objet finalisable B ne peut de façon fiable utiliser B dans un finaliseur et vice-versa. Il n’y a pas de notion d’ordonnancement dans les finaliseurs. De plus, les objets stockés dans des variables statiques seront collectés à un certain moment durant le déchargement du domaine d’application ou bien la fin du processus. Accéder à une variable statique qui référence un objet finalisable (ou bien appeler une méthode statique qui pourrait utiliser des valeurs stockées dans des variables statiques, comme par exemple les fonctions de trace) n’est pas sain, même s’il existe Environment.HasShutdownStarted (depuis la 1.1) qui peut détecter si le finaliseur s’exécute durant le déchargement d’un domaine d’application ou bien pendant l’interruption d’un processus.

 

Jeffrey Richter :

Remarquez qu’il est correct d’atteindre des types par valeur décompartimentées (unboxed).

 

  • N’appelez jamais directement le finaliseur. Ce n’est pas une opération légale en C#, mais elle l’est en VB et en C++ et est en fait du code IL vérifiable. Bien que ce guide vous oriente vers des méthodes de nettoyage qui résistent à des appels réalisés dans les pires circonstances, les développeurs devraient tout de même supposer que le finaliseur ne sera appelé que par le thread de finalisation de la CLR.
  • Le code de nettoyage doit résister à des appels multiples. Cela signifie que l’on doit mettre en place une mécanique qui enregistre le fait qu’un appel initial ait eu lieu dans une instance donnée. Les exemples précédents ont montré l’utilisation possible d’un booléen, mais on pourrait également annuler ou mettre à zéro une variable et effectuer un test sur cet état.

 

Voici des exemples où un code de nettoyage pourrait être exécuté plusieurs fois :

 

    1. Le code invoque lui-même plusieurs fois le finaliseur
    2. N’importe quel code client possédant une référence vers votre objet pourrait invoquer GC.ReRegisterForFinalize durant sa propre finalisation. Même si votre objet a déjà été finalisé, il sera malgré tout réintégré dans la logique de finalisation de la CLR.

 

  • Ne supposez pas que votre objet ne puisse être atteint durant la finalisation. D’autres objets toujours dans la file de finalisation pourraient avoir encore des références actives depuis lesquelles ils voudraient accéder à votre objet, éventuellement après sa finalisation. Vous devez être certain de détecter les états inconsistants durant l’exécution de n’importe quelle méthode pouvant compromettre la sécurité.

 

Brian Grunkemeyer :

Votre finaliseur peut parfaitement s’exécuter en même temps que des méthodes d’instances de votre objet. Si le finaliseur ferme une ressource utilisée par votre type, on peut alors devoir appeler GC.KeepAlive(this) à la fin de chaque méthode d’instance qui ne se sert pas du pointeur this après avoir effectuée quelques opération sur cette ressource. Si vous pouvez utiliser SafeHandle pour encadre la ressource, vous pouvez presque toujours supprimer le finaliseur de votre type, vous n’avez alors plus besoin de vous soucier de ce problème.

 

Note du traducteur :

Vous trouverez des informations concernant GC.KeepAlive() et le problème mentionné plus haut ici :

http://www.dotnetguru2.org/amethyste/index.php?p=206&more=1&c=1&tb=1&pb=1

 

  • Ne supposez pas que le finaliseur sera effectivement appelé. Dans certaines circonstances, rares, seuls les finaliseurs critiques seront exécutés. Il est même possible, plus rarement, qu’aucun finaliseur ne soit invoqué.

La question de savoir quand et comment cela peut arriver varie selon que l’on exécute du code dans un environnement hébergé ou non.

Dans le cas d’un code non hébergé, la finalisation peut ne jamais être lancée, par exemple:

-       Si l’arrêt d’un processus provoque un dépassement de délai (time-out)

-       Si un thread de finalisation est interrompu après avoir retiré l’objet de la pile de finalisation, mais avant d’avoir appelé le finaliseur. Dans ce cas .NET passe à l’objet suivant dans la pile.

-       Si un processus est arrêté sans passer par une interruption gérée (appel par P/Invoke à kernel32!ExitProcess).

Tout cela peut également se produire dans le cas d’un code hébergé, mais l’hôte peut ultérieurement initier un déchargement rude du domaine d’application. Dans ce cas, seuls les finaliseurs critiques seront appelés.

Par exemple dans le cas d’un code hébergé par SQLServer, un déchargement normal d’un domaine d’application peut se transformer en déchargement rude si un thread ou bien un finaliseur ne répond pas au bout d’un certain temps.

Même en dehors de telles situations, un objet finalisable avec une référence publiquement accessible peut avoir sa finalisation court-circuitée par n’importe quel code client non fiable (untrusted). Plus spécialement il peut appeler GC.SuppressFinalizable et empêcher ainsi la finalisation de se faire, y compris les finalisations critiques. Une bonne stratégie pour diminuer les risques est d’envelopper les ressources critiques dans une instance non publique qui expose un finaliseur. Tant que cette instance n’est pas passée au code appelant, il ne pourra pas annuler sa finalisation. Si vous utilisez SafeHandle dans votre code et ne l’exposez jamais au monde extérieur, vous pouvez garantir que la finalisation de la ressource sera faite (sous réserve, comme mentionné plus haut que l’implémentation du SafeHandle soit correcte).

  • Limitez l’écriture d’un objet supportant la finalisation critique (SafeHandle ou tout autre type dont la hiérarchie contient CriticalFinalizerObject) aux cas où le finaliseur doit absolument être exécuté, même lors d’un déchargement normal ou rude du domaine d’application. Plus d’informations plus loin dans le chapitre sur SafeHandle.

 

Note du traducteur :

CriticalFinalizerObject et la notion de finalisation critique est une nouveauté de .NET 2.0. On peut trouver des informations ici :

http://msdn2.microsoft.com/fr-fr/library/system.runtime.constrainedexecution.criticalfinalizerobject.aspx

Jusqu’à la version .NET 1.1, il n’existait aucun mécanisme garantissant la finalisation d’un objet. Cela pouvait poser des problèmes pour les applications à très haute disponibilité. La finalisation critique fournit un moyen de répondre à ce problème.

 

  • Evitez d’allouer de la mémoire dans un finaliseur. L’allocation pourrait échouer en raison d’un manque de mémoire, ce qui fait une cause supplémentaire d’échec possible du  finaliseur.
  • Ne pas allouer de la mémoire depuis un finaliseur critique ou bien depuis la méthode ReleaseHandle d’un SafeHandle. Ces méthodes sont placées dans des zones d’exécution contraintes (CER) conçues pour n’utiliser qu’un sous ensemble de .NET marqué avec un contrat de fiabilité. Si le finaliseur critique détecte une corruption du système ou bien reçoit de mauvais codes d’erreur de Windows le mieux reste de lever une exception (bien qu’il soit préférable de faire en sorte que SafeHandle.ReleaseHandle retourne false). Notez que les exceptions non gérées levées dans le thread de finalisation déstabiliseront le processus.

 

Note du traducteur :

La notion de zone d’exécution limitée (constrained execution region) est aussi une des nouveautés de .NET 2.0. Un article en français expliquant cette notion a été donnée dans le chapitre d’introduction.

 

  • Ne pas appeler de membre virtuel depuis un finaliseur sauf dans le cas d’une conception très contrôlée, par exemple Dispose(bool). Même dans ce cas des sous-classes malveillantes pourrait injecter du code nocif dans l’appel de la méthode virtuelle pouvant générer par exemple des exceptions non gérées. C’est une résultante du mécanisme de polymorphisme qui fera s’exécuter l’implémentation de niveau le plus bas dans la hiérarchie de la classe et il n’existe aucune garantie que le code se chaînera à la classe de base.

 

L’exemple C# qui suit montre un appel dynamique à Dispose.

L’auteur de la classe de base s’attend à voir s’exécuter la méthode Dispose de la classe de base (c’est à dire que chaque sous-classe chaînera en appelant base.Dispose comme se serait le cas automatiquement en C++). En fait la version dérivée de Dispose ne fait jamais cela. Les ressources de la classe de base ne sont donc jamais libérées et entraîne une fuite de mémoire.

Si la méthode Dispose() dérivée annule la finalisation, les ressources détenues par la classe de base ne seront pas libérées, pas même lors de la finalisation !

 

public class Base : IDisposable
{
    public virtual void Dispose() //BUG: cette méthode ne doit pas être virtuelle.
    {
        Console.WriteLine("Nettoyage de la classe de base");
        GC.SuppressFinalize(this);
    }

    ~Base()
    {
        Dispose();
    }
}

public class Derived : Base
{
    public override void Dispose()
    {
        Console.WriteLine("Nettoyage de la classe dérivée");
        GC.SuppressFinalize(this);
    }

    ~Derived() // BUG: on ne doit pas surcharger un finaliseur
    {
        Dispose();
    }

}

Parmi les solutions possibles à ce problème on pourrait rendre Dispose non virtuel, injecter une méthode privée DisposImpl destinée à appeler base.Dispose() que le finaliseur appellerait explicitement.

Il y a en effet un risque avec la méthode Dispose(Bool) du modèle Dispose que les sous-classes ne chaînent pas correctement avec sa classe de base.

Brian Grunkemeyer :

 

Avec le code précédent, le finaliseur de la classe dérivée sera exécuté et appelle la méthode Dispose() de la classe dérivée.

Le compilateur C# ajoute en plus un bloc try/finally à chaque finaliseur qui appelle le finaliseur de la classe de base. De cette façon, le finaliseur dérivé appellera le finaliseur de sa classe de base qui va virtuellement appeler Dispose. Le finaliseur dérivé sera donc appelé une seconde fois. Cet appel redondant illustre bien la raison pour laquelle une classe ne doit avoir qu’un seul finaliseur.

 

  • Ecrivez des finaliseurs tolérants à des instances partiellement construites, c’est à dire des objets dont le constructeur n’a pu aller jusqu’au bout. Vous devriez absolument valider toutes les hypothèses qui affectent le fonctionnement du finaliseur. Même les ressources allouées dans un le constructeur d’un objet peuvent ne pas être valides si une exception a été levée au milieu du code du constructeur. C’est plus fréquent qu’on pourra le croire. Si l’exception n’est pas explicitement levée depuis le constructeur, une classe dérivée pourrait le faire avant de chaîner le constructeur de la classe de base, le constructeur peut également être interrompu par une exception asynchrone levée par l’infrastructure .NET. Dans ce cas le finaliseur s’exécutera quand même, mais certains champs pourraient ne pas être initialisés.

Considérez le code suivant dans lequel list peut être null si le constructeur lève une exception avant de l’avoir assigné :

public class MyClass
{
    private ArrayList list;

    public MyClass()
    {
        // du code ici
        list = new ArrayList();
    }

    ~MyClass() //bug: list peut être null
    {
        foreach (IntPtr i in list)
        {
            CloseHandle(i);
        }
    }

}

On peut modifier ainsi le finaliseur afin d’éviter une NullReferenceException sur le thread de finalisation (qui entraîne la fermeture de l’application) :

~MyClass() // version corrigée
{
    if (list != null)
    {
        foreach (IntPtr i in list)
        {
            CloseHandle(i);
        }
    }

}

 

Jeffrey Richter :

Si un constructeur lève une exception, la CLR appelera quand même le finaliseur de l’objet. Par conséquent, les champs de l’objet peuvent ne pas être initialisés. Votre finaliseur doit prendre en compte ce type de problème.

 

  • Ecrire des finaliseurs qui sont agnostiques au thread. Les finaliseurs sont exécutés dans un ordre quelconque, sur n’importe quel thread et peuvent être appelés simultanément sur le même objet. En général, l’environnement d’exécution ne donne aucune garantie sur la politique engagée au sujet des threads, évitez donc d’écrire du code dépendant de l’implémentation actuelle de la politique de finalisation.

 

Chris Brumme :

J’ai décris l’environnement d’exécution des threads lors de la finalisation ici :

http://blogs.msdn.com/cbrumme/archive/2004/02/20/77460.aspx

Comme vous pouvez le constater, les techniques comme la résurrection peuvent faire que les threads applicatifs ou le thread de finalisation accèdent à votre objet de façon concurrente. En d’autres termes, du point de vue de la sécurité vous devez supposer un multi-threading massif. D’un autre côté vous devez aussi supposer qu’un seul thread sera actif dans votre objet lorsque vous le finaliserez ou le détruirez. Vous êtes libre d’ignorer cette possibilité aussi longtemps que cela ne créé pas de trous de sécurité. Dans ce cas vous devrez corriger votre code. Un moyen évident est de rendre la finalisation thread-safe.

 

StringBuilder est un excellent exemple. Si vous utilisez StringBuilder depuis plusieurs threads, le texte placé dans le buffer peut être collecté par le ramasse-miettes. C’est le problème du code client de l’objet.

Ce problème de multi-threading exposait les premières versions de StringBuilder à un trou de sécurité. Il était alors possible de créer des chaînes mutables dans lesquelles le résultat de StringBuilder.ToString() pouvait être modifié. C’est un grave problème de sécurité et il a été corrigé depuis.

Toutefois le problème d’intégrité des données n’est pas un trou de sécurité et ne sera jamais corrigé. Considérez le même genre de distinction avant de rendre ou non thread-safe vos finaliseurs.

 

Brian Grunkemeyer :

La CLR pourrait choisir d’utiliser un jour plusieurs threads de finalisation et il se pourrait aussi qu’ils soient extraient du pool de thread ou même que ce soit votre thread principal. On a besoin de la liberté de lancer un finaliseur sur n’importe quel thread.

 

Note du traducteur :

Un exemple concret de ce problème a été abordé par Nicole Calinoiu, une toute nouvelle MVP sécurité. Dans certains cas l’exécution d’un finaliseur peut lever une exception de sécurité :

http://spaces.msn.com/calinoiu/Blog/cns!1pYmj2Kpn4Oz9CW9IKwXQF8A!154.entry

 

  • Évitez de bloquer l’exécution depuis un finaliseur. Par exemple ne faites pas de synchronisation ou de verrouillage, ne lancez pas de méthodes telles System.Threading.Thread.Sleep(), à moins d’avoir identifié un vrai bug de sécurité ou de stress qui pourrait empêcher la finalisation de se terminer. De telles opérations peuvent différer ou même empêcher l’exécution complète d’autres finaliseurs dans la pile. C’est par exemple le cas si l’hôte remarque qu’une partie tarde à répondre, dans ce cas il peut déclencher un arrêt brutal. Si vous devez effectuer une opération thread-safe atomique, préférez la classe Interlocked, elle est légère et non bloquante.
  • Ne pas lever d’exceptions non gérées, ne pas laisser d’exceptions s’échapper de votre finaliseur, sauf celles vraiment critiques comme OutOfMemoryException. Cela pourrait déclencher l’arrêt de tout le processus (.NET 2.0), arrêter l’application et empêcher d’autres finaliseurs de s’exécuter de façon contrôlée.
  • Eviter de ressusciter vous-même vos objets en plaçant une référence dans une racine, c’est à dire une référence que le ramasse-miettes peut atteindre comme un champ static ou bien un objet vers lequel vous détenez une référence. Réenregistrer un objet à des implications sur les performances et peut provoquer des comportements inattendus. Les objets sur lesquels vous avez une référence peuvent avoir déjà été finalisés entièrement ce qui signifie qu’il n’est presque jamais sain de reprendre une exécution normale une fois que l’on a été placé dans la pile de finalisation. Si vous recyclez un objet, essayez de le faire dans Dispose() plutôt et dans le finaliseur uniquement en dernier recourt.

 

Note du traducteur :

Lorsqu’un objet réclame sa finalisation il est considéré comme mort. Plus tard le ramasse-miettes le réactive afin d’exécuter sa méthode Finalize, une fois celle-ci exécutée l’objet est considéré comme (définitivement) mort à nouveau. C’est ce phénomène de réactivation qui est appelé résurrection.

Ce dont on parle ici est le cas où le finaliseur place un pointeur de l’objet ressuscité vers une variable static comme par exemple :

 

class SomeType
{
    ~SomeType()
    {
        Application.PbjHolder = this;
    }
}

class Application
{
    public static Object ObjHolder;

}

Vous trouverez plus d’information dans le livre de Jeffrey Richter d’où cet exemple est tiré :

http://www.amazon.fr/exec/obidos/ASIN/0735614229/qid=1141464681/sr=8-8/ref=sr_8_xs_ap_i8_xgl14/171-2868251-6317819

Une application possible de la résurrection est justement les pools d’objets.

 

Rico Mariani :

Ce n’est pas un problème spécialement différent des autres problèmes liés à la finalisation. Si vous recyclez des ressources non gérées vous pouvez avoir déjà besoin d’un finaliseur. L’endroit où vous pouvez recycler est la méthode Dispose().

 

Lisez l’article suivant (Object : Release or Recycle ?) au chapitre « Option Two » :

http://blogs.msdn.com/ricom/archive/2004/02/11/71143.aspx

Supposer que la finalisation est un événement rare peut être une bonne approximation :

http://weblogs.asp.net/ricom/archive/2003/12/04/41281.aspx

Ce guide de développement n’a pas pour intention d’interdire les pools d’objet, mais plutôt de faire en sorte que vous avez réfléchi avec soin à votre stratégie de recyclage.

 

  • Ne supposez pas que si vous ne ressuscitez pas vous même votre objet vous êtes à l’abri d’être placé à nouveau dans une racine que ce soit après ou pendant la finalisation. Cela peut se produire par exemple si quelqu’un derrière vous dans la pile de finalisation détient une référence vers votre objet et se ressuscite. Puisque la finalisation se produit dans un ordre quelconque, il n’y a pas de garantie que cela ne se produira pas. Votre objet peut alors avoir été entièrement finalisé, mais d’autres objets peuvent tenter de l’utiliser comme s’il était encore actif. Dans ce cas vous devez vous assurer de ne pas casser les invariants de la classe qui pourraient ne pas être encore actifs après la finalisation.
    Il est préférable de lever une exception, c’est à dire traiter cette situation comme similaire à celle de l’utilisation d’un objet qui a été détruit avec Dispose comme on l’a vu précédemment.
  • Ne pas modifier le contexte de thread depuis un finaliseur car cela finira par polluer le thread du finaliseur. Souvenez vous que le finaliseur s’exécute sur un thread entièrement différent de ceux dans lesquels s’exécutait l’objet lorsqu’il était actif. Ne permettez pas à un thread de faire un emprunt d’identité, de le lier à une culture plus ou moins exotique…

 

Brian Grunkemeyer :

Considérez le thread de finalisation plutôt comme un thread extrait d’un pool – si vous le cassez en polluant son état, vous l’assumez.

 

  • Ne supposez pas que sous prétexte que tel objet puisse être atteint pendant que vous êtes finalisé qu’il n’a pas été ou n’est pas en train d’être finalisé : champ static ou environnement de développement par exemple. Lorsqu’un AppDomain est déchargé ou le processus interrompu, même les composants critiques de l’infrastructure peuvent avoir commencé ou terminé leur finalisation. Dans ce cas la méthode AppDomain.CurrentDomain.IsFinalizingForUnload() retourne true.

 

Brian Grunkemeyer :

Au tout début de l’implémentation de .NET 1.0, je déboguais un problème dans un finaliseur qui ne semblait pas être appelé. Pour m’aider à comprendre s’il était lancé ou non j’y ai placé un appel à Console.WriteLine. Résultat l’application a explosée aussitôt avec un ObjectDisposedException. Comment un simple Console.WriteLine dans un finaliseur peut produire un tel problème ?

 

En fait le flux sous-jacent à la console avait été finalisé avant mon instance. J’ai retenu cette leçon : utiliser uniquement des instances non finalisables dans un finaliseur.
Maintenant on ne ferme plus les handles pour stdout, stdin ou stderr. Ceci est utile pour les débogages et les logs ainsi que dans les contextes de domaine d’application multiple dans un même processus où l’on ne souhaite pas voir un quelconque AppDomain finaliser le handle de stdout.

 

Retenez donc : l’utilisation de Console dans un finaliseur est maintenant fiable, mais aucune garantie pour les autres classes.

 

  • Ne pas définir de finaliseurs sur des types par valeur. Seuls les types par référence sont finalisés par la CLR et donc toute tentative de placer un finaliseur dans un type par valeur sera ignorée. Les compilateurs C# et C++ lèvent une alerte.

Les finaliseurs C#

La méthode Finalize est héritée de la classe System.Object, bien que seules celles qui la redéfinissent soient sujet à être finalisées. C# dispose d’une syntaxe spéciale qui simplifie l’écriture d’un finaliseur et en fait vous empêche de surcharger Finalize comme on le ferait pour une méthode ordinaire. Par exemple :

public class Resource
{
    ~Resource()
    {
        // déallocation des ressources
    }

}

 

Sera transformé par le compilateur en ceci :

 

public class Resource
{
    protected override void Finalize()
    {
        try
        {
            // désalloue les ressources
        }
        finally
        {
            base.Finalize();
        }
    }

}

Ce dernier code ne se compilera pas, c’est la syntaxe standard qu’il convient d’utiliser.

Joe Duffy :

Dans les premiers temps de .NET, les finaliseurs étaient appelés explicitement destructeurs par les programmeurs C#. L’expérience aidant, nous essayons maintenant de promouvoir l’idée qu’en réalité Dispose est bien plus proche d’un destructeur au sens de C++ (déterministe), tandis que le finaliseur est quelque chose d’autre (non déterministe). Le fait que C# adopte la syntaxe C++ est certainement une source de confusion. Cette confusion a été dommageable et maintenant nous faisons une claire distinction entre les ressources et durée de vie.

 

Jeffrey Richter :

Il est malencontreux que les concepteurs de C# aient choisis la syntaxe en tilde pour définir ce qui est appelé un finaliseur. Les développeurs venant du monde C++ pensent de façon naturelle qu’il s’agit d’un nettoyage déterministe. Il aurait été préférable qu’une syntaxe différente soit adoptée et aiderai plus à faire comprendre qu’un environnement géré est très différent d’une architecture non gérée.

Exemples d’utilisation du modèle Dispose

Voici un exemple plus sophistiqué du modèle Dispose qui reprend tout ce qui a été dit précédemment.

#region using

using System;

using System.Security;

using System.ComponentModel;

using System.Runtime.ConstrainedExecution;

using System.Runtime.InteropServices;

#endregion

 

public class ComplexWindow : IDisposable
{

    private MySafeHandleSubclass handle; // pointeur vers une ressource
    private Component component; // une autre ressource
    private bool disposed = false; // trace si Dispose a déjà été appelé

    #region Constructeur

    /// <summary>
   /// Constructeur
    /// </summary>
    public ComplexWindow()
    {
        handle = CreateWindow("MyClass", "Test Window",
           0, 50, 50, 500, 900,
            IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

        component = new Component();
    }

    #endregion

    // implementation de IDisposable
    #region
Dispose
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed) // le code dans Dispose ne doit être exécuté qu'une seule fois
        {
            // if this is a dispose call dispose on all state you

            // hold, and take yourself off the Finalization queue.

            if (disposing)
            {
                // appel déterministe seulement
 

                if (handle != null)
                {
                    handle.Dispose();
                }

                if (component != null)
                {
                    component.Dispose();
                    component = null;
                }
            }

            // libérez ici vos propres états (objets non gérés)
            AdditionalCleanup();
            this.disposed = true; // on a déjà détruit l'objet
        }
    }

    #endregion

    #region Finaliseur
    ~ComplexWindow()
    {
        // le finaliseur se contente d'appeler Dispose
        Dispose(false);
    }

      #endregion

    #region AdditionalCleanup
    /// <summary>
    /// logique de nettoyage personnalisée
    /// </summary>
    private void AdditionalCleanup()
    {
        /*

         * Cette méthode ne doit pas faire d'allocation ou poser de verrous

         * sauf nécessité impérieuse en raison d'un problème de sécurité par exemple

         * puisqu'elle sera appelée durant la finalisation.

         * Elle est également sujette à toutes les restrictions relatives aux finaliseurs

         * */

    }
      #endregion

    #region ShowWindow

    // chaque fois que vous accédez à cette classe, vérifiez si son état disposed

    // est à true. dans ce cas levez une exception.

    public void ShowWindow()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException("");
        }

        // sinon fait son boulot
    }

    #endregion

 

    [DllImport("user32.dll", SetLastError = true,CharSet = CharSet.Auto, BestFitMapping = false)]
    private static extern MySafeHandleSubclass CreateWindow(
        string lpClassName, string lpWindowName, int dwStyle,
        int x, int y, int nWidth, int nHeight, IntPtr hwndParent,
        IntPtr Menu, IntPtr hInstance, IntPtr lpParam);

    #region SafeMyResourceHandle (internal)
    /// <summary>
    /// Implémentation d'un SafeHandle personnalisé
    /// </summary>
   internal sealed class SafeMyResourceHandle : SafeHandle
    {
        private HandleRef href;
 

        // appelé par P/Invoke lorsque la méthode externe retournera
        private SafeMyResourceHandle () : base(IntPtr.Zero, true)
        {
        }

         // Inutile de fournir un finaliseur. SafeHandle est un finaliseur critique et appelera pour

        // vous ReleaseHandle

        public override bool IsInvalid
        {

            get { return handle == IntPtr.Zero; }

        }

        override protected bool ReleaseHandle()
        {

            // Cette méthode est une zone d'exécution limitée (CER)

            // Elle ne doit donc pas faire d'allocation

            return DeleteObject(href);

        }

 

        [DllImport("gdi32.dll", SuppressUnmanagedCodeSecurity)]
        [ReliabilityContract(Consistency.WillNotCorruptState,CER.Success)]
        private static extern bool DeleteObject(HandleRef hObject);

        [DllImport("kernel32")]
        internal static extern SafeMyResourceHandle CreateHandle(int someState);

    }

      #endregion

}

 

// Classe dérivée

public class MyComplexWindow : ComplexWindow
{

    private Component myComponent; // encore une autre ressource

    private bool disposed = false;

 

    public MyComplexWindow()
    {

        myComponent = new Component();

    }

 

    #region Dispose

    protected override void Dispose(bool disposing)
    {

        if (!this.disposed) // code exécuté une seule fois
        {

            if (disposing)
            {

                // appel déterministe

                if (myComponent != null)
                {

                    myComponent.Dispose();

                }

                this.disposed = true;

            }

 

            // Effectue les autres nettoyages comme par exemple
            // vider les flux (Flush).

        }

        base.Dispose(disposing);

    }

    #endregion

}

 

Brian Grunkemeyer :

La classe dérivée MyComplexWindow n’a pas strictement besoin d’ajouter son propre indicateur disposed. Elle pourrait par exemple se contenter de vérifier si myComponent n’est pas null dans Dispose(bool) et dans chaque méthode utilisant ce type.

Dispose(bool) doit encore chaîner avec sa classe de base dans tous les cas toutefois. L’inconvénient de cette approche est qu’elle devient plus compliquée à maintenir si vous ajoutez d’autres champs dans votre type.

 

SafeHandler

 

Note du traducteur :

La classe SafeHandle est détaillée dans MSDN ici :

http://msdn2.microsoft.com/fr-fr/library/system.runtime.interopservices.safehandle.aspx

L’utilisation de classes héritant de SafeHandle permet d’envelopper un handle dans une ressource gérée. La classe fournit des moyens de protection contre les attaques par recyclage, un mécanisme de finalisation critique et un support spécial pour le marshalling géré/non géré. SafeHandle est une classe abstraite dont on doit hériter. Des exemples d’implémentation sont fournis par .NET comme SafeHandleZeroOrMinusOneIsInvalid.

 

  • Utilisez SafeHandle pour envelopper des ressources non gérées comme les handles de préférence à l’utilisation d’IntPtr ou Int32. Vous pouvez par exemple écrire :

    private SafeMyResourceHandle handle ;

Si votre ressource est très légère, par exemple un petit buffer non géré et n’est pas une cible pour une attaque par recyclage ou si votre scénario est très sensible aux performances, il est possible de ne pas utiliser SafeHandle. Cette classe à beaucoup d’avantages, comme par exemple la réduction du graphe de promotion du à la finalisation, protection contre les attaques par recyclage, garantie d’aucune fuite en cas de déchargement rude du domaine d’application, mais peut être relativement lourde dans certains cas.

 

Brian Grunkemeyer :

Nous avons implémenté dans .NET un certain nombre de SafeHandle pour des ressources assez fréquentes comme SafeFileHandle et SafeWaitHandle dans l’espace de nom Microsoft.Win32.SafeHandles. Pour une discussion plus approfondie lisez cet article :

http://blogs.msdn.com/bclteam/archive/2005/03/16/396900.aspx

 

HandleCollector et stress mémoire

 

  • Evitez de lancer des appels à GC.Collect() et GC.GetTotalMemory(true) dans l’intention de déclencher une collection par le ramasse-miettes. Un appel à GC.Collect() interfère avec le mécanisme naturel et impactera négativement les performances de votre application. Ces méthodes ont été conçues pour faire des tests. Utilisez plutôt la classe System.Runtime.InteropServices.HandleCollector ou bien lancez un appel à GC.AddMemoryPressure.

  HandleCollector

 

HandleCollector suit les handles non gérés de façon à déclencher une collection en réponse à la réalisation de certaines consignes spécifiées durant sa construction. Lorsque vous allouez une ressource gérée par un collecteur, appelez simplement Add(). Lorsqu’elle est libérée, appelez Remove(). Lorsque le nombre de handle dépasse un certain seuil, une collection est alors déclenchée.

 

Jeffrey Richter :

En interne, HandleCollector et AddMemoryPresure appellent GC.Collect. Ce que ce guide de conception essaye de dire est qu’il y a des moments plus appropriés que d’autres pour appeler GC.Collect, mais que d’une façon générale on doit essayer d’éviter le plus possible.

On doit avoir une vraie raison. HandleCollector et AddMemoryPresure existent pour de vraies raisons : il vaut mieux lancer une collection et altérer les performances que de voir l’application mal fonctionner en raison d’un manque de ressource non gérées.

 

Cet exemple montre comment les handles GDI peuvent être limités par un seuil entre 10 et 50. La frontière basse est une suggestion pour commencer à songer à une collection tandis que la limite haute implique obligatoirement une collection :

 

HandleCollector collector = new HandleCollector("GdiHandles", 10, 50);

IntPtr CreateSolidBrush()
{

    collector.Add();

 

    try
    {
        return CreateSolidBrushImpl();
    }
    catch
    {
        // si l'allocation échoue, décrémenter le nombre de handles actifs

        collector.Remove();

        throw;
    }
}

void DeleteBrush(IntPtr handle)
{

    DeleteObjectImpl(handle);

    collector.Remove();

}

 

Add/Remove MemoryPressure

 

GC.AddMemoryPressure indique au ramasse-miettes que le coût d’un objet géré est plus élevé qu’il n’apparaît sur la seule fois de ses ressources non gérées. On passe à cette fonction la taille des ressources non gérées, le ramasse-miettes adaptera sa stratégie de collection pour augmenter la pression sur cet objet.

GC.RemoveMemoryPressure réalise l’inverse une fois que la ressource a été libérée. L’exemple qui suit démontre ce mécanisme lors de l’allocation/désallocation d’un Bitmap:

 

public class Bitmap : IDisposable
{

    private long bmpSize;

    public Bitmap(string path)
    {
        bmpSize = new FileInfo(path).Length;

        GC.AddMemoryPressure(bmpSize);

        // travail d'allocation
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        // travail de nettoyage
        GC.RemoveMemoryPressure(bmpSize);
    }

    ~Bitmap()
    {
        Dispose(false);
    }

}

 

Si vous allouez un grand nombre de petits octets, il est toutefois plus efficace d’ajouter et enlever la pression sur des blocs plus larges. Par exemple des méga-octets ou des centaines de kilo-octets à la fois. Vous pourriez souhaiter implémenter un gestionnaire de pression personnalisé pour prendre en charge ce scénario du genre de l’exemple de code qui suit. Le BitmapPressureManager ajoute ou supprime une pression par blocs de 500 ko de mémoire. La classe Bitmap précédente a été modifiée pour appeler BitmapPressureManager à la place.

 

public class Bitmap : IDisposable
{

    private long bmpSize; 

    public Bitmap(string path) {

        bmpSize = new FileInfo(path).Length;
        BitmapPressureManager.AddMemoryPressure(bmpSize);
        // travail d'allocation
    }

    public void Dispose()    {

        Dispose(true);
        GC.SuppressFinalize(this);
    } 

    protected void Dispose(bool disposing)    {

        // travail de netoyage
        BitmapPressureManager.RemoveMemoryPressure(bmpSize);
    }

    ~Bitmap()
    {
        Dispose(false);
    }
}

internal static class BitmapPressureManager
{

    private const long threshold = 524288; // ajoute une pression par blocs de 500 ko

    private static long pressure;

    private static long committedPressure;

    private static readonly object sync = new object();

    internal static void AddMemoryPressure(long amount) {

        Interlocked.Add(ref pressure, amount);
        PressureCheck();
    }

    internal static void RemoveMemoryPressure(long amount)    {

        AddMemoryPressure(-amount);
    }

    private static void PressureCheck(){

        if (Math.Abs(pressure - committedPressure) >= threshold){

            lock (sync)   {
                long diff = pressure - committedPressure;
                if (Math.Abs(diff) >= threshold) // double contrôle
                {

                    if (diff < 0)
                    {
                        GC.RemoveMemoryPressure(-diff);
                    }
                    else
                    {
                        GC.AddMemoryPressure(diff);
                    }

                     committedPressure += diff;
                }
            }
        }
    }
}

Auteur : Joe Duffy