Le thread constructeur statique C # est-il sûr?

247

En d'autres termes, ce thread d'implémentation Singleton est-il sûr:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

    static Singleton()
    {
        instance = new Singleton();
    }

    public static Singleton Instance
    {
        get { return instance; }
    }
}
urini
la source
1
Il est thread-safe. Supposons que plusieurs threads souhaitent obtenir la propriété Instanceà la fois. L'un des threads sera invité à exécuter d'abord l'initialiseur de type (également connu sous le nom de constructeur statique). Pendant ce temps, tous les autres threads souhaitant lire la Instancepropriété seront verrouillés jusqu'à la fin de l'initialiseur de type. Ce n'est qu'après la fin de l'initialisation du champ que les threads seront autorisés à obtenir la Instancevaleur. Donc, personne ne peut voir l' Instanceêtre null.
Jeppe Stig Nielsen
@JeppeStigNielsen Les autres threads ne sont pas verrouillés. De ma propre expérience, j'ai eu des erreurs désagréables à cause de cela. La garantie est que seul le premier thread démarrera l'initialiseur statique ou le constructeur, mais les autres threads essaieront alors d'utiliser une méthode statique, même si le processus de construction ne s'est pas terminé.
Narvalex
2
@Narvalex Cet exemple de programme (source encodée en URL) ne peut pas reproduire le problème que vous décrivez. Cela dépend peut-être de la version du CLR que vous possédez?
Jeppe Stig Nielsen
@JeppeStigNielsen Merci d'avoir pris votre temps. Pouvez-vous s'il vous plaît m'expliquer pourquoi ici le champ est dominé?
Narvalex
5
@Narvalex Avec ce code, les majuscules Xfinissent par être -1 même sans thread . Ce n'est pas un problème de sécurité des threads. Au lieu de cela, l'initialiseur x = -1s'exécute en premier (il se trouve sur une ligne antérieure du code, un numéro de ligne inférieur). L'initialiseur X = GetX()s'exécute ensuite, ce qui rend les majuscules Xégales à -1. Et puis le constructeur statique "explicite", l'initialiseur de type static C() { ... }s'exécute, qui ne change que les minuscules x. Donc après tout cela, la Mainméthode (ou la Otherméthode) peut continuer et lire en majuscules X. Sa valeur sera -1, même avec un seul thread.
Jeppe Stig Nielsen

Réponses:

189

Les constructeurs statiques sont garantis pour être exécutés une seule fois par domaine d'application, avant la création de toute instance d'une classe ou l'accès à des membres statiques. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

L'implémentation indiquée est thread-safe pour la construction initiale, c'est-à-dire qu'aucun verrouillage ou test nul n'est requis pour construire l'objet Singleton. Cependant, cela ne signifie pas que toute utilisation de l'instance sera synchronisée. Il existe différentes façons de procéder; J'en ai montré un ci-dessous.

public class Singleton
{
    private static Singleton instance;
    // Added a static mutex for synchronising use of instance.
    private static System.Threading.Mutex mutex;
    private Singleton() { }
    static Singleton()
    {
        instance = new Singleton();
        mutex = new System.Threading.Mutex();
    }

    public static Singleton Acquire()
    {
        mutex.WaitOne();
        return instance;
    }

    // Each call to Acquire() requires a call to Release()
    public static void Release()
    {
        mutex.ReleaseMutex();
    }
}
Zooba
la source
53
Notez que si votre objet singleton est immuable, l'utilisation d'un mutex ou de tout mécanisme de synchronisation est une exagération et ne doit pas être utilisée. De plus, je trouve l'exemple d'implémentation ci-dessus extrêmement fragile :-). Tout le code utilisant Singleton.Acquire () devrait appeler Singleton.Release () lorsqu'il aura terminé d'utiliser l'instance singleton. À défaut de le faire (par exemple, retourner prématurément, laisser la portée via une exception, oublier d'appeler Release), la prochaine fois que ce Singleton sera accessible à partir d'un autre thread, il se bloquera dans Singleton.Acquire ().
Milan Gardian
2
D'accord, mais j'irais plus loin. Si votre singleton est immuable, l'utilisation d'un singleton est exagérée. Définissez simplement des constantes. En fin de compte, l'utilisation correcte d'un singleton nécessite que les développeurs sachent ce qu'ils font. Aussi fragile que soit cette implémentation, elle est toujours meilleure que celle de la question où ces erreurs se manifestent de manière aléatoire plutôt que comme un mutex manifestement inédit.
Zooba
26
Une façon de réduire la fragilité de la méthode Release () consiste à utiliser une autre classe avec IDisposable comme gestionnaire de synchronisation. Lorsque vous acquérez le singleton, vous obtenez le gestionnaire et pouvez placer le code nécessitant le singleton dans un bloc using pour gérer la version.
CodexArcanum
5
Pour les autres qui pourraient être déclenchés par ceci: Tous les membres de champ statiques avec des initialiseurs sont initialisés avant l'appel du constructeur statique.
Adam W. McKinley
12
La réponse ces jours-ci est d'utiliser Lazy<T>- toute personne qui utilise le code que j'ai publié à l'origine le fait mal (et honnêtement, ce n'était pas si bon au début - il y a 5 ans), je n'étais pas aussi bon dans ce domaine que l'actuel -moi, ce est :) ).
Zooba
86

Bien que toutes ces réponses donnent la même réponse générale, il y a une mise en garde.

N'oubliez pas que toutes les dérivations potentielles d'une classe générique sont compilées en tant que types individuels. Soyez donc prudent lorsque vous implémentez des constructeurs statiques pour les types génériques.

class MyObject<T>
{
    static MyObject() 
    {
       //this code will get executed for each T.
    }
}

ÉDITER:

Voici la démonstration:

static void Main(string[] args)
{
    var obj = new Foo<object>();
    var obj2 = new Foo<string>();
}

public class Foo<T>
{
    static Foo()
    {
         System.Diagnostics.Debug.WriteLine(String.Format("Hit {0}", typeof(T).ToString()));        
    }
}

Dans la console:

Hit System.Object
Hit System.String
Brian Rudolph
la source
typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha
6
Je pense que c'est le point que j'essaie de faire valoir. Les types génériques sont compilés en tant que types individuels en fonction des paramètres génériques utilisés, de sorte que le constructeur statique peut et sera appelé plusieurs fois.
Brian Rudolph
1
C'est le cas lorsque T est de type valeur, pour le type de référence T, un seul type générique serait généré
sll
2
@sll: Not True ... Voir mon montage
Brian Rudolph
2
Un cosntructeur intéressant mais vraiment statique appelé pour tous les types, juste essayé pour plusieurs types de référence
sll
28

L'utilisation d'un constructeur statique est en fait threadsafe. Le constructeur statique ne peut être exécuté qu'une seule fois.

De la spécification du langage C # :

Le constructeur statique d'une classe s'exécute au plus une fois dans un domaine d'application donné. L'exécution d'un constructeur statique est déclenchée par le premier des événements suivants à se produire dans un domaine d'application:

  • Une instance de la classe est créée.
  • Tous les membres statiques de la classe sont référencés.

Alors oui, vous pouvez être sûr que votre singleton sera correctement instancié.

Zooba a fait valoir un excellent point (et 15 secondes avant moi aussi!) Que le constructeur statique ne garantira pas un accès partagé thread-safe au singleton. Cela devra être traité d'une autre manière.

Parc Derek
la source
8

Voici la version Cliffnotes de la page MSDN ci-dessus sur c # singleton:

Utilisez toujours le modèle suivant, vous ne pouvez pas vous tromper:

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

   private Singleton(){}

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

Au-delà des fonctionnalités évidentes de singleton, il vous offre ces deux choses gratuitement (en ce qui concerne singleton en c ++):

  1. construction paresseuse (ou pas de construction si elle n'a jamais été appelée)
  2. synchronisation
Jay Juch
la source
3
Paresseux si la classe n'est pas associée à d'autres statiques non liées (comme les consts). Sinon, l'accès à toute méthode ou propriété statique entraînera la création d'une instance. Je ne dirais donc pas que c'est paresseux.
Schultz9999
6

Les constructeurs statiques sont garantis de se déclencher une seule fois par domaine d'application, votre approche doit donc être correcte. Cependant, elle n'est fonctionnellement pas différente de la version en ligne plus concise:

private static readonly Singleton instance = new Singleton();

La sécurité des threads est plus un problème lorsque vous initialisez des choses paresseusement.

Andrew Peters
la source
4
Andrew, ce n'est pas entièrement équivalent. En n'utilisant pas de constructeur statique, certaines des garanties sur le moment où l'initialiseur sera exécuté sont perdues. Veuillez consulter ces liens pour une explication détaillée: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park
Derek, je connais bien "l'optimisation" de beforefieldinit mais, personnellement, je ne m'en soucie jamais.
Andrew Peters
lien de travail pour le commentaire de @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Ce lien semble périmé
phoog
4

Le constructeur statique terminera son exécution avant qu'un thread ne soit autorisé à accéder à la classe.

    private class InitializerTest
    {
        static private int _x;
        static public string Status()
        {
            return "_x = " + _x;
        }
        static InitializerTest()
        {
            System.Diagnostics.Debug.WriteLine("InitializerTest() starting.");
            _x = 1;
            Thread.Sleep(3000);
            _x = 2;
            System.Diagnostics.Debug.WriteLine("InitializerTest() finished.");
        }
    }

    private void ClassInitializerInThread()
    {
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() starting.");
        string status = InitializerTest.Status();
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() status = " + status);
    }

    private void classInitializerButton_Click(object sender, EventArgs e)
    {
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
    }

Le code ci-dessus a produit les résultats ci-dessous.

10: ClassInitializerInThread() starting.
11: ClassInitializerInThread() starting.
12: ClassInitializerInThread() starting.
InitializerTest() starting.
InitializerTest() finished.
11: ClassInitializerInThread() status = _x = 2
The thread 0x2650 has exited with code 0 (0x0).
10: ClassInitializerInThread() status = _x = 2
The thread 0x1f50 has exited with code 0 (0x0).
12: ClassInitializerInThread() status = _x = 2
The thread 0x73c has exited with code 0 (0x0).

Même si l'exécution du constructeur statique a pris du temps, les autres threads se sont arrêtés et ont attendu. Tous les threads lisent la valeur de _x définie en bas du constructeur statique.

Idées commerciales Philip
la source
3

La spécification Common Language Infrastructure garantit qu '"un initialiseur de type doit s'exécuter exactement une fois pour tout type donné, sauf s'il est explicitement appelé par le code utilisateur". (Section 9.5.3.1.) Donc, à moins que vous ayez un IL fou sur l'appel libre Singleton ::. Cctor directement (peu probable), votre constructeur statique s'exécutera exactement une fois avant l'utilisation du type Singleton, une seule instance de Singleton sera créée, et votre propriété d'instance est thread-safe.

Notez que si le constructeur de Singleton accède à la propriété Instance (même indirectement), la propriété Instance sera null. Le mieux que vous puissiez faire est de détecter quand cela se produit et de lever une exception, en vérifiant que l'instance n'est pas nulle dans l'accesseur de propriété. Une fois votre constructeur statique terminé, la propriété Instance sera non nulle.

Comme le souligne la réponse de Zoomba, vous devrez sécuriser Singleton pour accéder à partir de plusieurs threads, ou implémenter un mécanisme de verrouillage autour de l'utilisation de l'instance singleton.

Dominic Cooney
la source
2

Juste pour être pédant, mais il n'y a pas de constructeur statique, mais plutôt des initialiseurs de type statique, voici une petite démonstration de la dépendance cyclique du constructeur statique qui illustre ce point.

Florian Doyon
la source
1
Microsoft semble en désaccord. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92
0

Bien que d'autres réponses soient pour la plupart correctes, il y a encore une autre mise en garde avec les constructeurs statiques.

Conformément à la section II.10.5.3.3 Courses et blocages de l' infrastructure linguistique commune ECMA-335

L'initialisation de type à elle seule ne doit pas créer un blocage sauf si un code appelé depuis un initialiseur de type (directement ou indirectement) appelle explicitement des opérations de blocage.

Le code suivant entraîne un blocage

using System.Threading;
class MyClass
{
    static void Main() { /* Won’t run... the static constructor deadlocks */  }

    static MyClass()
    {
        Thread thread = new Thread(arg => { });
        thread.Start();
        thread.Join();
    }
}

L'auteur original est Igor Ostrovsky, voir son article ici .

oleksii
la source