Petit rappel sur le Disposing Pattern en C#
Ca fait longtemps que je veux faire ce post, au sujet d’une question trop souvent négligée : le disposing pattern. On peut voir ça comme un petit rappel, ou un post à classer dans la catégorie « back to basics ». En deux mots, il s’agit de gérer correctement la libération des ressources non-managées, dans un programme managé.
EDIT : Ce post est un poisson d’avril, évidemment. Il vaudrait mieux ne pas utiliser l’implémentation du disposing pattern proposée…
Quelques rappels
On parle de code managé en .NET car on fait implicitement référence au garbage collector qui gère automatiquement la mémoire. Il n’est pas question de faire un cours sur le GC, mais retenez juste qu’il est très intelligent, et qu’il est indépendant. Tellement indépendant qu’on ne peut pas prévoir quand est-ce qu’il va réellement effectuer son recyclage. Par exemple, lorsque vous écrivez :
public int Add(int a, int b) { Calculator calculator = new Calculator(); return calculator.Add(a, b); };
Vous créez une nouvelle instance de Calculator
qui sera référencée par la variable locale calculator
. A la sortie de la méthode, la variable locale n’existe plus, et l’instance de Calculator
n’est donc plus référencée. L’espace mémoire qu’elle occupe peut être récupéré, et le GC le sait. (Notez l’assonance.) Il y a un instant j’insistais sur l’indépendance du garbage collector : s’il sait qu’il peut libérer une instance, vous ne savez pas quand il le fera.
On considère que la collecte des miettes est indéterministe. Vous n’avez aucun moyen de savoir précisément quand le calculator que vous avez instancié précédemment sera collecté. Ca ne sera pas forcément à la sortie de la méthode, comme on pourrait naïvement s’y attendre ; ça peut être 47 ms ou 47 secondes après (pour caricaturer) et vous devriez même être heureux de ne pas avoir à vous en soucier.
Mais.
Tout n’est pas managé. Parfois, vous utilisez à partir d’objets managés des ressources non managées. Même si ça peut paraitre évident, il est bon de revenir là dessus. On parle de « ressource non managée » dans ce contexte pour décrire une ressource :
- Dont l’allocation est déclenchée à partir de votre code managé.
- Qui doit être libérée.
- Mais dont le garbage collector n’est pas responsable.
Les handles de fichiers, les connexions de bases de données, les sockets réseau ou les références COM sont des exemples de ressources non managées.
Comment implémenter le disposing pattern
On se base sur l’interface IDisposable
, et sur les destructeurs de C#. IDisposable
expose une méthode Dispose
, qui va détruire l’objet, et faire en sorte que sa mémoire soit désallouée du heap (et non pas de la pile comme je le vois souvent dans des blogs), surtout si on fait bien appel au mot clé using
qui ne sert pas qu’à importer des namespaces.
Mais assez de théorie, passons au code. Imaginons la classe C# suivante, et regardons comment elle fait pour libérer le IntPtr
qu’elle a en variable publique et qui constitue sa variable non managée dangereuse (attention aux memory leaks lol) :
EDIT : Ce post est un poisson d’avril, évidemment. Il vaudrait mieux ne pas utiliser l’implémentation du disposing pattern proposée…
public class MaClasseManagée : IDisposable // on implémente IDisposable { private IntPtr m_Ressource_non_managée; // Attention ! bool disposed = false; // pour savoir si la classe est libérée public MaClasseManagée(IntPtr ptrnonmanagé) { if(ptrnonmanagé != null) { m_Ressource_non_managée = ptrnonmanagé; // allocation mémoire non managée } else // sinon { if (ptr.Equals(IntPtr.Zero) == true) { // on désalloue directement la mémoire du pointeur m_Ressource_non_managée = IntPtr.Zero } GC.CancelFullGCNotification(); // pas obligé, mais il vaut mieux } } public void FaireQuelqueChose() { // ici on utilise la ressource non managée qui a été allouée // ... // ... } // destruteur de la class ~MaClasseManagée() { // partie un peu tricky : on interdit au CLR de supprimer l'instance // courante car ça a déjà été fait dans le Dispose d'IDisposable GC.SuppressFinalize(this); // on s'assure quand même qu'on garde pas de référence vers le pointeur m_Ressource_non_managée = IntPtr.Zero; } // methode dispose la plus importante public void Dispose() { if(!disposed) { // on appelle le garbage collector, et on force la libération de // tous les objets pour être certains de plus référencer des variables // non managées depuis C# GC.Collect(); disposed = true; // déja disposé } } }
Notez donc qu’il faut bien faire la différence entre le constructeur, le destructeur, et le dispose. Trop souvent les gens oublient l’un des trois, et compromettent du coup la sécurité de leur code.
A l’utilisation c’est simple :
// mot clé using using(MaClasseManagée obj = new MaClasseManagée()) { // manipuler obj obj.FaireQuelqueChose(); } // à la sortie du bloc using, le garbage collector est appellé et libère le tas // des instances de MaClasseManagée persistantes, donc, forcément avec notre // pattern, des ressources non managées IntPtr aussi
Et voilà :)
Je compte faire un article bientôt sur une nouvelle façon d’appréhender cette problématique avec l’AOP, notamment dans les architectures distribuées où il y a trop de code à écrire pour qu’on puisse se permettre de perdre du temps avec ces considérations bas niveau dans un contexte économique délicat (c’est la crise !) qui nous force à économiser le temps et l’argent sans pour autant arrêter de produire des logiciels de qualité qui donneront satisfaction au client et permettront aux développeurs de ne pas s’arracher les cheveux sur des solutions de 50 projets où les autres ont fait n’importe quoi et qu’il faut quand même livrer coute que coute tant pis pour les nuits blanches (ça serait quand même un comble de ne plus être en mesure de maitriser son métier, ou de devoir arrêter la qualité sous prétexte que c’est les chefs de projets qui décident des planning sans rien comprendre de la technique alors que c’est la base).
C’est quand même plus simple d’implémenter System.IUnknown et d’appeler AddRef et Release au bon moment. Et puis pour vraiment sûr que la mémoire est libérée, il vaut mieux faire dans le Dispose :
Excellent billet. Je voulais juste signaler que pour les raisons évoquées ci-dessus, on préfère parler de Finaliseur pour la méthode ~MaClasseManagée(), et de Destructeur pour la méthode public void Dispose(). Ce qui en même temps rend le tout cohérent avec l’implémentation de ce pattern en C++ managé (je crois).
J’avais un très bon lien sur les discussions autour de ce pattern, que je mettrai ici. Mais uniquement si je le retrouve ;-)
Merci pour ce post. C’est important de rappeler les fondamentaux.
Yann> Tu aurais pu préciser que sur les systèmes 64 bits, ton implémentation du EMC (Exhaustive Memory Collect) devait itérer de 0 à long.MaxValue. Le truc qui est cool, c’est que ça fonctionne aussi en 32bit.
Mac> Puisque nous ne sommes plus le premier avril, je peux tenter une réponse sérieuse :
En C# : Il n’y a pas de destructeur à proprement parler. On utilise la syntaxe C++ d’un destructeur, mais le compilateur traduit ça en un appel au Finalizer. Dispose est juste une méthode comme les autres, sa seule spécificité est qu’elle est reconnue par certaines constructions du langage, et types du framework. On est trop libre d’en faire ce qu’on veut, et d’implémenter IDisposable n’importe comment.
En C++/CLI : Quand on ajoute un destructeur à sa classe, le compilateur rend le type IDisposable, et traduit le destructeur en Dispose. Pour implémenter un finalizer, il faut utiliser une nouvelle syntaxe ( !MaClasseManagée() )
En voyant le titre, j’ai pensé qu’il s’agissait d’un autre sujet : où et comment libérer les ressources non-managées.
Le faire dans un finalize (donc quand le GC passe) n’est pas toujours une bonne idée. Un exemple simple, imaginez un système qui manque de sockets mais pas de mémoire.
Évidemment, le 1er avril, la prod peut corriger efficacement ce problème en retirant une barrette mémoire :-)
Une seule chose à dire, vivent les pointeurs et les programmeurs qui savent s’en servir… ;)
héhéhéhé