Traduction

Ceci est la traduction la plus fidèle possible de l'article de Jon Skeet, Implementing the Singleton Pattern in C#Implementing the Singleton Pattern in C#.

Introduction

Il y a de nombreuses façons d'implémenter le pattern singleton en C#. Je les présenterai ici dans l'ordre inverse d'élégance, en commençant avec le plus communément vu (qui n'est pas « thread-safe ») et j'exposerai ensuite des versions avec chargement différé total, thread-safe, simple et hautement performante.

Toutes ces implémentations partagent quatre caractéristiques communes :

  • un constructeur unique, qui est privé et sans paramètre. Ceci empêche d'autres classes de l'instancier (ce qui serait une violation du pattern). Notez que cela empêche aussi les sous-classes - si un singleton peut être sous-classé une fois, il peut être sous-classé deux fois, et si chacune de ces sous-classes peut créer une instance, le pattern est violé. Le pattern factory peut être utilisé si vous avez besoin d'une unique instance d'un type de base, mais le type exact n'est pas connu jusqu'au runtime ;
  • la classe est sealed. Ceci n'est pas nécessaire, strictement parlant, dû au point précédent, mais cela peut aider le JIT pour optimiser un peu plus certains éléments ;
  • une variable statique qui maintient une référence à l'instance unique créée, le cas échéant ;
  • un accès public est un moyen d'obtenir la référence à l'instance unique créée, créez-en un si nécessaire.

Notez que toutes ces implémentations utilisent aussi une propriété publique statique nommée Instance comme moyen d'accéder à l'instance. Dans tous les cas, la propriété peut être facilement convertie en une méthode, sans impact sur la notion de thread-safety ou sur les performances.

Première version - Non thread-safe

 
Sélectionnez
// Bad code! Do not use!
public sealed class Singleton
{
    private static Singleton instance=null;

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (instance==null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

Comme expliqué précédemment, l'exemple ci-dessus n'est pas thread-safe. Deux threads différents pourraient tous les deux avoir évalué le test if (instance == null) et obtenir vrai comme résultat, les deux créeront une instance, ce qui viole le pattern singleton. Notez qu'en fait, l'instance peut déjà avoir été créée avant l'évaluation de l'expression, mais le système de la mémoire ne garantit pas que la nouvelle valeur de l'instance soit vue par d'autres threads tant que les barrières adéquates de la mémoire n'ont été franchies.

Seconde version - Simple thread-safety

 
Sélectionnez
public sealed class Singleton
{
    private static Singleton instance = null;
    private static readonly object padlock = new object();

    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }
}

Cette implémentation est thread-safe. Le thread met un verrou sur un objet partagé, et ensuite vérifie si l'instance a été créée ou non avant de créer l'instance. Ceci prend en compte le problème des barrières de mémoire (le verrouillage permet de s'assurer que toutes les lectures se font logiquement après l'acquisition de verrou, et le déverrouillage s'assure que toutes les écritures se font logiquement avant la libération du verrou) et assure que seulement un seul thread créera une instance (étant donné qu'un seul thread peut être dans cette partie du code à un moment - au moment où le second thread rentre dedans, le premier thread aura créé l'instance, l'expression sera donc évaluée à faux). Malheureusement, les performances en souffrent car un verrou est obtenu à chaque fois que l'instance est requise.

Notez qu'à la place de verrouiller sur le type Singleton comme certaines versions de cette implémentation font, je verrouille sur la valeur d'une variable statique qui est privée à la classe. Verrouiller des objets auxquels d'autres classes peuvent accéder et donc verrouiller (tout comme le type) conduit à des problèmes de performance, voire même des deadlocks. Ceci est une préférence générale pour ma part. Autant que possible, verrouillez seulement des objets créés spécifiquement dans le but du verrouillage, ou alors dont la documentation autorise leur utilisation dans des cas spécifiques (exemple pour attendre/remplir une file d'attente). Habituellement, de tels objets devraient être privés dans la classe où ils sont utilisés. Cela rend vraiment plus facile l'écriture d'applications thread-safe.

Troisième version - Tenter thread-safety en utilisant une double vérification du verrouillage

 
Sélectionnez
// Bad code! Do not use!
public sealed class Singleton
{
    private static Singleton instance = null;
    private static readonly object padlock = new object();

    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

Cette implémentation tente d'être thread-safe sans nécessairement effectuer un verrouillage à chaque fois. Malheureusement, il y a quatre inconvénients avec ce pattern :

  • cela ne fonctionne pas en Java. Cela peut être une chose étrange à lire, mais il est bon à savoir si vous avez besoin du pattern Singleton en Java. Les développeurs C# peuvent d'ailleurs aussi être des développeurs Java. Le système de mémoire en Java ne garantit pas que le constructeur soit terminé avant que la référence du nouvel objet soit assignée à l'instance. Le système de mémoire Java a subi un remaniement lors de la version 1.5, mais la double vérification du verrouillage ne fonctionne toujours pas sans une variable volatile (comme en C#) ;
  • sans aucune barrière en mémoire, cela ne fonctionne pas non plus avec la spécification ECMA CLI. Il est possible que sous le modèle de mémoire de .NET Framework 2.0 (qui est plus important que la spécification ECMA), cela fonctionne, mais je ne préfère pas m'appuyer sur ces sémantiques plus fortes, notamment sur des situations où des experts ne sont pas d'accord sur ce qui est bon ou pas ! ;
  • il est facile de se tromper. Le pattern a besoin d'être presque identique que précédemment - tous changements importants sont susceptibles d'impacter les performances ou l'exactitude ;
  • cela ne fonctionne toujours pas ainsi comme pour les implémentations ultérieures.

Quatrième version - Pas tout à fait en différé, mais thread-safe sans utiliser de verrous

 
Sélectionnez
public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}

Comme vous pouvez voir, ceci est vraiment extrêmement simple - mais pourquoi est-ce thread-safe et en quoi est-ce différé ? Eh bien, les constructeurs statiques en C# sont utilisés pour s'exécuter seulement lorsqu'une instance de la classe est créée ou qu'un membre statique est référencé et pour s'exécuter seulement une fois par AppDomain. Étant donné que cette vérification pour le type en cours de construction a besoin d'être exécutée, peu importe ce qui se passe, il sera plus rapide plutôt que d'ajouter des vérifications supplémentaires comme dans les exemples précédents. Il y a cependant plusieurs soucis inattendus :

  • ce n'est pas aussi différé que les autres implémentations. En particulier, si vous avez des membres statiques autres que Instance, la première référence à ces membres conduira à la création de l'instance. Ceci est corrigé dans la prochaine implémentation ;
  • il y a des complications si un premier constructeur statique invoque un autre qui invoque encore le premier. Regardez les spécifications .NET (actuellement à la section 9.5.3 de la partition 2) pour plus de détails concernant la nature exacte des initialiseurs de type - ils sont peu susceptibles de vous mordre, mais il est bon d'être au courant des conséquences des constructeurs statiques qui se référencent chacun dans un cycle ;
  • les initialiseurs de type en différé sont garantis uniquement par .NET lorsque le type n'est pas marqué avec un flag spécial appelé beforefieldinit. Malheureusement, le compilateur C# (fourni depuis le runtime .NET 1.1) marque tous les types qui n'ont pas un constructeur statique (exemple un bloc qui ressemble à un constructeur mais qui est marqué statique) avec beforefieldinit. J'ai à présent un article avec plus d'informations sur ce problème. Notez aussi que cela affecte les performances, comme décrit plus tard dans cet article.

Un raccourci que vous pouvez prendre avec cette implémentation (et seulement celui-là) est de simplement rendre la variable public static readonly et se débarrasser complètement de la propriété. Cela rend le code squelette de base vraiment court ! De nombreuses personnes, cependant, préfèrent avoir une propriété en cas de nouvelles actions qui seraient obligatoires dans le futur, et le JIT offre des performances probablement identiques. (Notez que le constructeur statique lui-même est toujours requis si vous avez besoin du mode différé.)

Cinquième version - Instanciation totalement différée

 
Sélectionnez
public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance { get { return Nested.instance; } }
        
    private class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}

Ici, l'instanciation est déclenchée par la première référence au membre statique de la classe imbriquée, qui apparait uniquement dans Instance. Cela signifie que l'implémentation est complètement différée mais a tous les avantages de performance des implémentations précédentes. Notez d'ailleurs que les classes imbriquées ont accès aux membres privés de la classe entourant, le contraire n'est pas vrai, d'où le besoin pour instance d'être internal dans ce cas. Cela ne pose pas d'autres problèmes que la classe soit privée. Cependant, le code est un petit peu plus compliqué afin de rendre l'instanciation différée.

Sixième version - Utiliser le type Lazy<T> de .NET 4

Si vous utilisez .NET 4 (ou une version supérieure), vous pouvez utiliser le type System.Lazy<T> pour rendre le mode différé vraiment simple. Tout ce que vous devez faire est de fournir un délégué au constructeur qui appelle le constructeur Singleton - ce qui est fait beaucoup plus simplement avec une expression lambda.

 
Sélectionnez
public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());
    
    public static Singleton Instance { get { return lazy.Value; } }

    private Singleton()
    {
    }
}

C'est simple et ça fonctionne bien. Cela vous permet aussi de vérifier si oui ou non l'instance a été créée avec la propriété IsValueCreated, si vous en avez besoin.

Performance contre mode différé

Dans la plupart des cas, vous n'aurez pas besoin d'un mode différé complet - à moins que votre initialisation de classe fasse quelque chose particulièrement consommateur de temps, ou qu'elle ait des effets secondaires à certains endroits, il est probablement bien de laisser de côté le constructeur statique explicite montré plus haut. Cela peut augmenter les performances car il permet au compilateur JIT de faire une unique vérification (pour une instance au début d'une méthode) pour s'assurer que le type a été initialisé, et qu'il soit ainsi considéré à partir de là. Si l'instance de votre singleton est référencée dans une boucle relativement restrictive, cela peut avoir une différence de performance significative (mais relative). Vous devriez décider si oui ou non l'instanciation totalement différée est nécessaire, et documenter votre décision au sein de la classe. (Voir ci-dessous plus d'informations sur les performances.)

Exceptions

Parfois, vous avez besoin de prendre en compte la possibilité d'avoir une exception à partir du constructeur d'un singleton, sans que celle-ci ne soit fatale à l'entièreté de l'application. Potentiellement, l'application peut être capable de gérer le problème en essayant encore. Utiliser les initialiseurs de type pour construire le singleton devient à ce moment-là problématique. Différents runtimes gèrent ce cas différemment, mais je ne sais pas lesquels prendront en compte correctement ce cas et même si un le faisait, votre code ne fonctionnerait pas sur les autres runtimes. Pour éviter ces problèmes, je suggérerais d'utiliser le second pattern listé sur cette page - utilisez simplement un verrou, et repassez à chaque fois par ce contrôle, construire l'instance dans la méthode/propriété si elle n'a pas encore été créée.

Merci à Andriy Tereshchenko d'avoir soulevé ce problème.

Un mot sur la performance

Beaucoup de raisons dans cet article découlent de personnes essayant d'être plus intelligentes en venant avec l'algorithme de double vérification du verrouillage. Il y a une habitude du verrouillage qui devient chère et donc peu judicieuse. J'ai écrit un rapide benchmark qui génère des instances de singletons dans une boucle essayant différentes variables un milliard de fois. Ce n'est absolument pas dans une optique scientifique étant donné qu'en réalité vous voudriez savoir à quelle vitesse on est si chaque itération implique en fait un appel à une méthode allant chercher le singleton, etc. Cependant, cela peut montrer un point important. Sur mon ordinateur portable, la solution la plus lente (avec un facteur d'environ 5) est celle de verrouillage (solution 2). Est-ce important ? Probablement non lorsque vous avez en tête qu'il parvenait tout de même à obtenir le singleton un milliard de fois en moins de 40 secondes. (Note : cet article avait été rédigé il y a un moment. Je m'attendrais à de meilleures performances à présent.) Cela signifie que vous récupérez le singleton 400 000 fois par seconde, le coût de cette récupération va augmenter les performances d'un pour cent - donc améliorer cela ne va pas changer grand-chose. Maintenant, si vous récupérez un singleton aussi souvent - est-ce que cela signifie que vous êtes en train de le faire dans une boucle ? Si vous vous souciez beaucoup du fait d'améliorer les performances un petit peu plus, pourquoi pas déclarer une variable locale en dehors de la boucle, récupérer la variable une fois et ensuite, faire votre boucle. Bingo, même les implémentations les plus lentes sont utilisables.

Je serais vraiment intéressé de voir une vraie application où la différence entre utiliser le verrouillage simple et une des solutions les plus rapides donnerait une différence de performance notable.

Conclusion

(Modifié légèrement le 7 janvier 2006 ; mise à jour le 12 février 2012.)

Il y a différentes façons d'implémenter le pattern singleton en C#. Un lecteur m'a écrit pour me détailler la façon dont il a encapsulé l'aspect synchronisation, et pour laquelle j'étais d'accord que cela peut être utile dans certaines situations bien précises (plus particulièrement lorsque vous avez besoin de très bonnes performances, et un besoin d'avoir la possibilité de déterminer si oui ou non le singleton a été créé, et aussi gérer les instanciations totalement différées quels que soient les membres statiques appelés). Je n'ai personnellement pas vu ce genre de situation assez souvent mais cela mérite d'être mentionné dans cet article, donc n'hésitez pas à me contacter si vous êtes dans cette situation.

Ma préférence porte sur la solution 4 : les seules fois où je devrais changer seraient lorsque je désire pouvoir appeler d'autres méthodes statiques sans déclencher l'initialisation, ou si j'avais besoin de savoir si oui ou non le singleton a été instancié. Je ne me rappelle pas la dernière fois où j'ai été dans cette situation, si elle s'est déjà produite. Dans ce cas, j'opterais pour la solution numéro 2 qui est simple et facile à mettre en place.

La solution 5 est élégante mais plus compliquée que la 2 et la 4 et comme je l'ai dit précédemment, les avantages qu'elle procure semblent être rarement utiles. La solution 6 est une manière très simple d'utiliser un mécanisme différé, si vous utilisez .NET 4. Cela a aussi l'avantage d'être évidemment différé. Je tends actuellement pour toujours utiliser la solution 4, tout simplement par habitude - mais si j'étais en train de travailler avec des développeurs inexpérimentés, j'irais plus probablement vers la solution 6 pour commencer avec un modèle simple et universellement applicable.

(Je ne voudrais pas utiliser la solution 1 car elle ne fonctionne pas, et je n'utiliserais pas la solution 3 car elle n'a aucun avantage de plus que la 5.)

Remerciements

Je tiens à remercier Jon Skeet pour son aimable autorisation de traduire cet article, ainsi que ClaudeLELOUP pour sa relecture attentive et ses corrections.