Il thread del costruttore statico C # è sicuro?


247

In altre parole, questo thread di implementazione di Singleton è sicuro:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

1
È sicuro per i thread. Supponiamo che diversi thread desiderino ottenere la proprietà Instancecontemporaneamente. A uno dei thread verrà chiesto di eseguire prima l'inizializzatore del tipo (noto anche come costruttore statico). Nel frattempo, tutti gli altri thread che desiderano leggere la Instanceproprietà verranno bloccati fino al termine dell'inizializzazione del tipo. Solo dopo la conclusione dell'inizializzatore di campo, i thread potranno ottenere il Instancevalore. Quindi nessuno può vedere l' Instanceessere null.
Jeppe Stig Nielsen,

@JeppeStigNielsen Gli altri thread non sono bloccati. Dalla mia esperienza ho avuto brutti errori per questo. La garanzia è che solo il thread del pugno avvierà l'inizializzatore statico o il costruttore, ma poi gli altri thread proveranno a utilizzare un metodo statico, anche se il processo di costruzione non è terminato.
Narvalex,

2
@Narvalex Questo programma di esempio (sorgente codificata in URL) non è in grado di riprodurre il problema descritto. Forse dipende da quale versione del CLR hai?
Jeppe Stig Nielsen,

@JeppeStigNielsen Grazie per aver dedicato del tempo. Potete per favore spiegarmi perché qui il campo è scavalcato?
Narvalex,

5
@Narvalex Con quel codice, le maiuscole Xfiniscono per essere -1 anche senza thread . Non è un problema di sicurezza del thread. Al contrario, l'inizializzatore x = -1viene eseguito per primo (è su una riga precedente nel codice, un numero di riga inferiore). Quindi viene X = GetX()eseguito l'inizializzatore , il che rende maiuscolo Xuguale a -1. E poi il costruttore "esplicito" statico, viene static C() { ... }eseguito il tipo inizializzatore , che cambia solo in minuscolo x. Quindi, dopo tutto ciò, il Mainmetodo (o Othermetodo) può andare avanti e leggere in maiuscolo X. Il suo valore sarà -1, anche con un solo thread.
Jeppe Stig Nielsen,

Risposte:


189

I costruttori statici sono garantiti per essere eseguiti solo una volta per dominio dell'applicazione, prima che vengano create le istanze di una classe o l'accesso ai membri statici. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

L'implementazione mostrata è thread-safe per la costruzione iniziale, ovvero non è richiesto alcun blocco o test null per costruire l'oggetto Singleton. Tuttavia, ciò non significa che qualsiasi utilizzo dell'istanza sarà sincronizzato. Ci sono molti modi in cui ciò può essere fatto; Ne ho mostrato uno qui sotto.

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();
    }
}

53
Nota che se il tuo oggetto singleton è immutabile, usare un mutex o qualsiasi meccanismo di sincronizzazione è eccessivo e non dovrebbe essere usato. Inoltre, trovo l'implementazione di esempio sopra estremamente fragile :-). Si prevede che tutto il codice che utilizza Singleton.Acquire () chiami Singleton.Release () quando viene fatto usando l'istanza singleton. In caso contrario (ad es. Ritorno prematuro, uscita dall'ambito tramite eccezione, dimenticanza di chiamare Release), la prossima volta che si accede a Singleton da un thread diverso, si bloccherà in Singleton.Acquire ().
Milano Gardian,

2
D'accordo, anche se andrei oltre. Se il tuo singleton è immutabile, l'uso di un singleton è eccessivo. Definisci solo le costanti. In definitiva, l'utilizzo di un singleton richiede correttamente che gli sviluppatori sappiano cosa stanno facendo. Per quanto fragile sia questa implementazione, è ancora meglio di quello nella domanda in cui quegli errori si manifestano casualmente piuttosto che come un mutex ovviamente inedito.
Zooba,

26
Un modo per ridurre la fragilità del metodo Release () è utilizzare un'altra classe con IDisposable come gestore di sincronizzazione. Quando acquisisci il singleton, ottieni il gestore e puoi inserire il codice che richiede il singleton in un blocco using per gestire il rilascio.
CodexArcanum,

5
Per altri che potrebbero essere inciampati in questo modo: qualsiasi membro di campo statico con inizializzatori viene inizializzato prima che venga chiamato il costruttore statico.
Adam W. McKinley,

12
La risposta in questi giorni è usare Lazy<T>- chiunque usi il codice che ho pubblicato in origine lo sta facendo in modo sbagliato (e onestamente non era così buono per cominciare - 5 anni fa - io non ero bravo in questa roba come attuale -me is :)).
Zooba,

86

Mentre tutte queste risposte danno la stessa risposta generale, c'è un avvertimento.

Ricorda che tutte le potenziali derivazioni di una classe generica vengono compilate come singoli tipi. Quindi, prestare attenzione quando si implementano costruttori statici per tipi generici.

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

MODIFICARE:

Ecco la dimostrazione:

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()));        
    }
}

Nella console:

Hit System.Object
Hit System.String

typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha,

6
Penso che sia questo il punto che sto cercando di chiarire. I tipi generici vengono compilati come singoli tipi in base ai parametri generici utilizzati, pertanto il costruttore statico può e verrà chiamato più volte.
Brian Rudolph,

1
Questo è giusto quando T è di tipo valore, per il tipo di riferimento T verrebbe generato un solo tipo generico
sll

2
@sll: Not True ... Vedi la mia modifica
Brian Rudolph,

2
Costruttore interessante ma davvero statico chiamato per tutti i tipi, appena provato per più tipi di riferimento
sll

28

L'uso di un costruttore statico in realtà è thread-safe. Il costruttore statico è garantito per essere eseguito una sola volta.

Dalle specifiche del linguaggio C # :

Il costruttore statico per una classe viene eseguito al massimo una volta in un determinato dominio dell'applicazione. L'esecuzione di un costruttore statico è innescata dal primo dei seguenti eventi che si verificano all'interno di un dominio dell'applicazione:

  • Viene creata un'istanza della classe.
  • Si fa riferimento a qualsiasi membro statico della classe.

Quindi sì, puoi fidarti che il tuo singleton verrà correttamente istanziato.

Zooba ha sottolineato in modo eccellente (e anche 15 secondi prima di me!) Che il costruttore statico non garantirà l'accesso condiviso sicuro al thread al singleton. Ciò dovrà essere gestito in un altro modo.


8

Ecco la versione di Cliffnotes dalla pagina MSDN sopra su c # singleton:

Usa sempre il seguente schema, non puoi sbagliare:

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

   private Singleton(){}

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

Oltre alle ovvie caratteristiche di singleton, ti dà queste due cose gratis (rispetto a singleton in c ++):

  1. costruzione pigra (o nessuna costruzione se non è mai stata chiamata)
  2. sincronizzazione

3
Lazy se la classe è non ha altre statiche non correlate (come i contro). In caso contrario, l'accesso a qualsiasi metodo o proprietà statica comporterà la creazione dell'istanza. Quindi non lo definirei pigro.
Schultz9999,

6

I costruttori statici sono garantiti per essere attivati ​​una sola volta per dominio app quindi il tuo approccio dovrebbe essere OK. Tuttavia, non è funzionalmente diverso dalla versione in linea più concisa:

private static readonly Singleton instance = new Singleton();

La sicurezza del thread è più un problema quando si inizializzano pigramente cose.


4
Andrew, non è del tutto equivalente. Non usando un costruttore statico, alcune delle garanzie su quando verrà eseguito l'inizializzatore andranno perse. Si prega di consultare questi collegamenti per una spiegazione approfondita: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Derek, ho familiarità con l' ottimizzazione preliminare , ma, personalmente, non me ne preoccupo mai.
Andrew Peters,

link funzionante per il commento di @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Questo link sembra essere obsoleto
phoog

4

Il costruttore statico terminerà l' esecuzione prima che qualsiasi thread possa accedere alla 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();
    }

Il codice sopra ha prodotto i risultati seguenti.

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).

Anche se il costruttore statico impiegava molto tempo a girare, gli altri thread si fermarono e attesero. Tutti i thread leggono il valore di _x impostato nella parte inferiore del costruttore statico.


3

La specifica di Common Language Infrastructure garantisce che "un inizializzatore di tipo deve essere eseguito esattamente una volta per un determinato tipo, a meno che non sia esplicitamente chiamato dal codice utente". (Sezione 9.5.3.1.) Quindi, a meno che tu non abbia qualche IL bizzarro sulla chiamata libera Singleton ::. Cctor direttamente (improbabile) il tuo costruttore statico verrà eseguito esattamente una volta prima che venga utilizzato il tipo Singleton, verrà creata solo un'istanza di Singleton, e la tua proprietà Instance è thread-safe.

Nota che se il costruttore di Singleton accede alla proprietà Instance (anche indirettamente), la proprietà Instance sarà nulla. Il meglio che puoi fare è rilevare quando ciò accade e generare un'eccezione, controllando che l'istanza non sia nulla nell'accessorio proprietà. Al termine del costruttore statico, la proprietà Instance sarà non nulla.

Come sottolinea la risposta di Zoomba, dovrai rendere Singleton sicuro per accedere da più thread o implementare un meccanismo di blocco usando l'istanza singleton.




0

Sebbene le altre risposte siano per lo più corrette, c'è ancora un altro avvertimento con i costruttori statici.

Ai sensi dell'articolo II.10.5.3.3 Razze e situazioni di stallo del Infrastructure ECMA-335 Common Language

L'inizializzazione del tipo da sola non deve creare un deadlock a meno che un codice chiamato da un inizializzatore di tipo (direttamente o indirettamente) invochi esplicitamente operazioni di blocco.

Il codice seguente provoca un deadlock

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'autore originale è Igor Ostrovsky, vedi il suo post qui .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.