Ist der statische C # -Konstruktor-Thread sicher?

247

Mit anderen Worten, ist dieser Singleton-Implementierungsthread sicher:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

    public static Singleton Instance
    {
        get { return instance; }
    }
}
Urini
quelle
1
Es ist threadsicher. Angenommen, mehrere Threads möchten die Eigenschaft Instancegleichzeitig erhalten. Einer der Threads wird angewiesen, zuerst den Typinitialisierer (auch als statischer Konstruktor bezeichnet) auszuführen. In der Zwischenzeit werden alle anderen Threads, die die InstanceEigenschaft lesen möchten, gesperrt, bis der Typinitialisierer abgeschlossen ist. Erst nach Abschluss des Feldinitialisierers können Threads den InstanceWert abrufen. So kann niemand das InstanceSein sehen null.
Jeppe Stig Nielsen
@JeppeStigNielsen Die anderen Threads sind nicht gesperrt. Aus eigener Erfahrung habe ich dadurch böse Fehler bekommen. Die Garantie ist, dass nur der erste Thread den statischen Initialisierer oder Konstruktor startet, aber die anderen Threads versuchen, eine statische Methode zu verwenden, selbst wenn der Konstruktionsprozess nicht abgeschlossen wurde.
Narvalex
2
@Narvalex Dieses Beispielprogramm (Quelle in URL codiert) kann das von Ihnen beschriebene Problem nicht reproduzieren. Vielleicht hängt es davon ab, welche Version der CLR Sie haben?
Jeppe Stig Nielsen
@JeppeStigNielsen Danke, dass du dir Zeit genommen hast. Können Sie mir bitte erklären, warum hier das Feld überschrieben wird?
Narvalex
5
@Narvalex mit diesem Code, Großbuchstaben Xenden als -1 auch ohne Einfädeln . Es ist kein Thread-Sicherheitsproblem. Stattdessen wird der Initialisierer x = -1zuerst ausgeführt (er befindet sich in einer früheren Zeile im Code, einer niedrigeren Zeilennummer). Dann wird der Initialisierer ausgeführt X = GetX(), wodurch Großbuchstaben Xgleich werden -1. Und dann wird der "explizite" statische Konstruktor, der Typinitialisierer static C() { ... }, ausgeführt, der nur Kleinbuchstaben ändert x. Nach all dem kann die MainMethode (oder OtherMethode) fortfahren und Großbuchstaben lesen X. Sein Wert wird -1auch mit nur einem Thread sein.
Jeppe Stig Nielsen

Antworten:

189

Statische Konstruktoren werden garantiert nur einmal pro Anwendungsdomäne ausgeführt, bevor Instanzen einer Klasse erstellt oder auf statische Mitglieder zugegriffen wird. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

Die gezeigte Implementierung ist für die anfängliche Konstruktion threadsicher, dh für die Konstruktion des Singleton-Objekts sind keine Sperren oder Nulltests erforderlich. Dies bedeutet jedoch nicht, dass eine Verwendung der Instanz synchronisiert wird. Es gibt verschiedene Möglichkeiten, dies zu tun. Ich habe unten einen gezeigt.

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
quelle
53
Beachten Sie, dass die Verwendung eines Mutex oder eines Synchronisationsmechanismus ein Overkill ist und nicht verwendet werden sollte, wenn Ihr Singleton-Objekt unveränderlich ist. Außerdem finde ich die obige Beispielimplementierung extrem spröde :-). Es wird erwartet, dass der gesamte Code, der Singleton.Acquire () verwendet, Singleton.Release () aufruft, wenn die Singleton-Instanz verwendet wird. Wenn Sie dies nicht tun (z. B. vorzeitige Rückkehr, Verlassen des Gültigkeitsbereichs über eine Ausnahme, Vergessen, Release aufzurufen), wird beim nächsten Zugriff auf diesen Singleton von einem anderen Thread aus ein Deadlock in Singleton.Acquire () ausgelöst.
Milan Gardian
2
Einverstanden, obwohl ich noch weiter gehen würde. Wenn Ihr Singleton unveränderlich ist, ist die Verwendung eines Singletons übertrieben. Definieren Sie einfach Konstanten. Um einen Singleton richtig zu verwenden, müssen die Entwickler wissen, was sie tun. So spröde diese Implementierung auch ist, sie ist immer noch besser als die in der Frage, bei der sich diese Fehler zufällig manifestieren, als als offensichtlich unveröffentlichter Mutex.
Zooba
26
Eine Möglichkeit, die Sprödigkeit der Release () -Methode zu verringern, besteht darin, eine andere Klasse mit IDisposable als Synchronisierungshandler zu verwenden. Wenn Sie den Singleton erwerben, erhalten Sie den Handler und können den Code, der den Singleton benötigt, in einen using-Block einfügen, um die Freigabe zu handhaben.
CodexArcanum
5
Für andere, die möglicherweise dadurch gestolpert werden: Alle statischen Feldelemente mit Initialisierern werden initialisiert, bevor der statische Konstruktor aufgerufen wird.
Adam W. McKinley
12
Die Antwort lautet heutzutage Lazy<T>: Jeder, der den Code verwendet, den ich ursprünglich gepostet habe, macht es falsch (und ehrlich gesagt war es anfangs nicht so gut - vor 5 Jahren war ich in diesem Bereich nicht so gut wie aktuell -me ist :)).
Zooba
86

Während alle diese Antworten die gleiche allgemeine Antwort geben, gibt es eine Einschränkung.

Denken Sie daran, dass alle möglichen Ableitungen einer generischen Klasse als einzelne Typen kompiliert werden. Seien Sie daher vorsichtig, wenn Sie statische Konstruktoren für generische Typen implementieren.

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

BEARBEITEN:

Hier ist die Demonstration:

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

In der Konsole:

Hit System.Object
Hit System.String
Brian Rudolph
quelle
typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha
6
Ich denke, das ist der Punkt, den ich versuche zu machen. Generische Typen werden als einzelne Typen kompiliert, auf deren Grundlage generische Parameter verwendet werden, sodass der statische Konstruktor mehrmals aufgerufen werden kann und wird.
Brian Rudolph
1
Dies ist richtig, wenn T vom
Werttyp
2
@sll: Nicht wahr ... Siehe meine Bearbeitung
Brian Rudolph
2
Interessanter, aber wirklich statischer Cosntructor für alle Typen, nur für mehrere Referenztypen
ausprobiert
28

Mit eigentlich ein statischer Konstruktor ist THREAD. Der statische Konstruktor wird garantiert nur einmal ausgeführt.

Aus der C # -Sprachenspezifikation :

Der statische Konstruktor für eine Klasse wird in einer bestimmten Anwendungsdomäne höchstens einmal ausgeführt. Die Ausführung eines statischen Konstruktors wird durch das erste der folgenden Ereignisse ausgelöst, die innerhalb einer Anwendungsdomäne auftreten:

  • Eine Instanz der Klasse wird erstellt.
  • Auf alle statischen Mitglieder der Klasse wird verwiesen.

Ja, Sie können darauf vertrauen, dass Ihr Singleton korrekt instanziiert wird.

Zooba machte einen hervorragenden Punkt (und auch 15 Sekunden vor mir!), Dass der statische Konstruktor keinen thread-sicheren gemeinsamen Zugriff auf den Singleton garantiert. Das muss auf andere Weise gehandhabt werden.

Derek Park
quelle
8

Hier ist die Cliffnotes-Version von der obigen MSDN-Seite auf c # singleton:

Verwenden Sie immer das folgende Muster, Sie können nichts falsch machen:

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

   private Singleton(){}

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

Abgesehen von den offensichtlichen Singleton-Funktionen erhalten Sie diese beiden Dinge kostenlos (in Bezug auf Singleton in C ++):

  1. fauler Bau (oder kein Bau, wenn es nie genannt wurde)
  2. Synchronisation
Jay Juch
quelle
3
Faul, wenn die Klasse keine andere nicht verwandte Statik hat (wie consts). Andernfalls wird beim Zugriff auf eine statische Methode oder Eigenschaft eine Instanz erstellt. Also würde ich es nicht faul nennen.
Schultz9999
6

Statische Konstruktoren werden garantiert nur einmal pro App-Domäne ausgelöst, daher sollte Ihr Ansatz in Ordnung sein. Es unterscheidet sich jedoch funktional nicht von der prägnanteren Inline-Version:

private static readonly Singleton instance = new Singleton();

Die Thread-Sicherheit ist eher ein Problem, wenn Sie Dinge träge initialisieren.

Andrew Peters
quelle
4
Andrew, das ist nicht ganz gleichwertig. Wenn Sie keinen statischen Konstruktor verwenden, gehen einige der Garantien für die Ausführung des Initialisierers verloren. Weitere Informationen finden Sie unter diesen Links: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park
Derek, ich bin mit der "Optimierung" vor dem Start vertraut , aber ich persönlich mache mir darüber keine Sorgen.
Andrew Peters
Arbeitslink für @ DerekParks Kommentar: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Dieser Link scheint veraltet zu sein: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
Phoog
4

Der statische Konstruktor wird beendet , bevor ein Thread auf die Klasse zugreifen darf.

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

Der obige Code ergab die folgenden Ergebnisse.

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

Obwohl die Ausführung des statischen Konstruktors lange gedauert hat, wurden die anderen Threads angehalten und gewartet. Alle Threads lesen den Wert von _x, der am unteren Rand des statischen Konstruktors festgelegt ist.

Handelsideen Philip
quelle
3

Die Common Language Infrastructure-Spezifikation garantiert, dass "ein Typinitialisierer für einen bestimmten Typ genau einmal ausgeführt wird, sofern er nicht ausdrücklich vom Benutzercode aufgerufen wird". (Abschnitt 9.5.3.1.) Wenn Sie also nicht direkt (unwahrscheinlich) eine verrückte IL auf dem losen aufrufenden Singleton ::. Cctor haben, wird Ihr statischer Konstruktor genau einmal ausgeführt, bevor der Singleton-Typ verwendet wird. Es wird nur eine Instanz von Singleton erstellt. und Ihre Instance-Eigenschaft ist threadsicher.

Beachten Sie, dass die Instance-Eigenschaft null ist, wenn der Singleton-Konstruktor (auch indirekt) auf die Instance-Eigenschaft zugreift. Das Beste, was Sie tun können, ist zu erkennen, wann dies geschieht, und eine Ausnahme auszulösen, indem Sie überprüfen, ob die Instanz im Eigenschaften-Accessor nicht null ist. Nach Abschluss Ihres statischen Konstruktors ist die Instance-Eigenschaft nicht null.

Wie die Antwort von Zoomba zeigt, müssen Sie Singleton für den Zugriff von mehreren Threads sicher machen oder einen Sperrmechanismus für die Verwendung der Singleton-Instanz implementieren.

Dominic Cooney
quelle
2

Nur um pedantisch zu sein, aber es gibt keinen statischen Konstruktor, sondern statische Typinitialisierer. Hier ist eine kleine Demo der Abhängigkeit von zyklischen statischen Konstruktoren, die diesen Punkt veranschaulicht.

Florian Doyon
quelle
1
Microsoft scheint nicht zuzustimmen. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92
0

Obwohl andere Antworten größtenteils richtig sind, gibt es noch eine weitere Einschränkung bei statischen Konstruktoren.

Gemäß Abschnitt II.10.5.3.3 Rennen und Deadlocks der ECMA-335 Common Language Infrastructure

Die Typinitialisierung allein darf keinen Deadlock erzeugen, es sei denn, ein von einem Typinitialisierer (direkt oder indirekt) aufgerufener Code ruft explizit Blockierungsvorgänge auf.

Der folgende Code führt zu einem 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();
    }
}

Der ursprüngliche Autor ist Igor Ostrovsky, siehe seinen Beitrag hier .

oleksii
quelle