Czy wątek konstruktora statycznego C # jest bezpieczny?

247

Innymi słowy, czy ten wątek implementacyjny Singleton jest bezpieczny:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

    public static Singleton Instance
    {
        get { return instance; }
    }
}
urini
źródło
1
Jest bezpieczny dla wątków. Załóżmy, że kilka wątków chce uzyskać tę właściwość Instancejednocześnie. Jeden z wątków zostanie poproszony o uruchomienie inicjatora typu (znanego również jako konstruktor statyczny). W międzyczasie wszystkie inne wątki, które chcą odczytać Instancewłaściwość, zostaną zablokowane do momentu zakończenia inicjalizacji typu. Dopiero po zakończeniu inicjalizacji pola wątki będą mogły uzyskać Instancewartość. Więc nikt nie widzi Instancebytu null.
Jeppe Stig Nielsen
@JeppeStigNielsen Pozostałe wątki nie są zablokowane. Z własnego doświadczenia mam z tego powodu okropne błędy. Gwarancją jest to, że tylko pierwszy wątek uruchomi inicjalizator statyczny lub konstruktor, ale wtedy inne wątki spróbują zastosować metodę statyczną, nawet jeśli proces budowy się nie zakończy.
Narvalex,
2
@Narvalex Ten przykładowy program (źródło zakodowane w adresie URL) nie może odtworzyć opisanego problemu. Może zależy to od posiadanej wersji CLR?
Jeppe Stig Nielsen,
@JeppeStigNielsen Dzięki za poświęcenie czasu. Czy możesz mi wyjaśnić, dlaczego tutaj pole jest nadpisane?
Narvalex,
5
@Narvalex Przy tym kodzie wielkie litery Xkończą się -1 nawet bez wątków . To nie jest kwestia bezpieczeństwa wątków. Zamiast tego inicjator x = -1uruchamia się pierwszy (znajduje się we wcześniejszym wierszu kodu, niższym numerze wiersza). Następnie uruchamia się inicjalizator X = GetX(), dzięki czemu wielkie litery są Xrówne -1. Następnie uruchamiany static C() { ... }jest „jawny” konstruktor statyczny, inicjalizator typu , który zmienia tylko małe litery x. Po tym wszystkim Mainmetoda (lub Othermetoda) może kontynuować i czytać wielkie litery X. Jego wartość będzie -1, nawet z jednym wątkiem.
Jeppe Stig Nielsen

Odpowiedzi:

189

Gwarantuje się, że statyczne konstruktory są uruchamiane tylko raz na domenę aplikacji, przed utworzeniem instancji klasy lub dostępem do elementów statycznych. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

Przedstawiona implementacja jest wątkowo bezpieczna dla początkowej konstrukcji, to znaczy, nie jest wymagane blokowanie ani testowanie zerowe do budowy obiektu Singleton. Nie oznacza to jednak, że wszelkie użycie instancji zostanie zsynchronizowane. Można to zrobić na wiele sposobów; Pokazałem jeden poniżej.

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
źródło
53
Zauważ, że jeśli twój obiekt singleton jest niezmienny, użycie muteksu lub dowolnego mechanizmu synchronizacji jest przesadą i nie powinno być używane. Ponadto powyższa przykładowa implementacja jest dla mnie wyjątkowo krucha :-). Oczekuje się, że cały kod korzystający z Singleton.Acquire () będzie wywoływał Singleton.Release (), gdy zostanie wykonany przy użyciu instancji singleton. Jeśli tego nie zrobisz (np. Przedwcześnie powrócisz, pozostawisz zakres przez wyjątek, zapominasz wywołać Release), następnym razem, gdy dostęp do Singletona zostanie uzyskany z innego wątku, nastąpi zakleszczenie w Singleton.Acquire ().
Milan Gardian,
2
Zgoda, choć pójdę dalej. Jeśli twój singleton jest niezmienny, użycie singletona to przesada. Po prostu zdefiniuj stałe. Ostatecznie prawidłowe użycie singletona wymaga, aby programiści wiedzieli, co robią. Choć ta implementacja jest krucha, wciąż jest lepsza niż ta w pytaniu, w którym błędy te manifestują się losowo, a nie jako ewidentnie niepublikowany muteks.
Zooba
26
Jednym ze sposobów zmniejszenia kruchości metody Release () jest użycie innej klasy z IDisposable jako procedury obsługi synchronizacji. Kiedy nabywasz singletona, dostajesz moduł obsługi i możesz umieścić kod wymagający singletona w bloku używającym do obsługi wydania.
CodexArcanum,
5
W przypadku innych, którzy mogą się z tego powodu skorzystać: Wszelkie elementy pola statycznego z inicjalizatorami są inicjowane przed wywołaniem konstruktora statycznego.
Adam W. McKinley,
12
W dzisiejszych czasach odpowiedź brzmi: używać Lazy<T>- każdy, kto używa kodu, który pierwotnie opublikowałem, robi to źle (i szczerze mówiąc, początkowo nie było tak dobrze - 5 lat temu - ja nie byłem tak dobry w takich sprawach jak obecnie -me is :)).
Zooba
86

Chociaż wszystkie te odpowiedzi dają tę samą ogólną odpowiedź, jest jedno zastrzeżenie.

Pamiętaj, że wszystkie potencjalne pochodne klasy ogólnej są zestawiane jako poszczególne typy. Dlatego należy zachować ostrożność przy wdrażaniu konstruktorów statycznych dla typów ogólnych.

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

EDYTOWAĆ:

Oto demonstracja:

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

W konsoli:

Hit System.Object
Hit System.String
Brian Rudolph
źródło
typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha
6
Myślę, że o to właśnie staram się zrobić. Typy ogólne są kompilowane jako pojedyncze typy, w oparciu o które używane są parametry ogólne, więc konstruktor statyczny może i będzie wywoływany wiele razy.
Brian Rudolph
1
Jest to słuszne, gdy T jest typu wartości, dla typu referencyjnego T zostałby wygenerowany tylko jeden typ ogólny
wszystkie
2
@sll: Not True ... Zobacz moją edycję
Brian Rudolph
2
Interesujący, ale naprawdę statyczny cosntructor wezwał do wszystkich typów, po prostu wypróbowałem dla wielu typów referencji
sll
28

Korzystanie z konstruktora statycznego jest w rzeczywistości bezpieczne dla wątków. Konstruktor statyczny może zostać wykonany tylko raz.

Ze specyfikacji języka C # :

Konstruktor statyczny dla klasy wykonuje się najwyżej raz w danej domenie aplikacji. Wykonanie konstruktora statycznego jest wyzwalane przez pierwsze z następujących zdarzeń, które mają miejsce w domenie aplikacji:

  • Utworzono instancję klasy.
  • Odwołanie dotyczy dowolnego statycznego elementu klasy.

Więc tak, możesz ufać, że twój singleton zostanie poprawnie utworzony.

Zooba doskonale podkreśliła (i 15 sekund przede mną!), Że statyczny konstruktor nie gwarantuje bezpiecznego wątku wspólnego dostępu do singletonu. Trzeba to potraktować w inny sposób.

Derek Park
źródło
8

Oto wersja Cliffnotes z powyższej strony MSDN na c # singleton:

Użyj następującego wzoru, zawsze nie możesz się pomylić:

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

   private Singleton(){}

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

Poza oczywistymi funkcjami singletonu, daje ci te dwie rzeczy za darmo (w odniesieniu do singletonu w c ++):

  1. leniwa konstrukcja (lub brak konstrukcji, jeśli nigdy nie została wywołana)
  2. synchronizacja
Jay Juch
źródło
3
Leniwy, jeśli klasa nie ma innych niepowiązanych statystyk (jak consts). W przeciwnym razie dostęp do dowolnej metody statycznej lub właściwości spowoduje utworzenie instancji. Więc nie nazwałbym tego leniwym.
Schultz9999
6

Konstruktory statyczne mają gwarancję, że będą uruchamiane tylko raz na domenę aplikacji, więc twoje podejście powinno być OK. Jednak funkcjonalnie nie różni się od bardziej zwięzłej, wbudowanej wersji:

private static readonly Singleton instance = new Singleton();

Bezpieczeństwo wątków jest większym problemem, gdy leniwie inicjujesz różne rzeczy.

Andrew Peters
źródło
4
Andrzej, to nie jest w pełni równoważne. Nieużywanie statycznego konstruktora powoduje utratę niektórych gwarancji dotyczących tego, kiedy inicjator zostanie wykonany. Szczegółowe linki znajdują się w poniższych linkach: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park
Derek, znam „optymalizację” przedfieldinit, ale osobiście nigdy się tym nie martwię.
Andrew Peters,
działający link do komentarza @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Ten link wydaje się być nieaktualny: ondotnet.com/pub/a/dotnet/2003/07/07/07/staticxtor.html
phoog
4

Konstruktor statyczny zakończy działanie wcześniej dowolny wątek dostęp do klasy.

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

Powyższy kod wygenerował poniższe wyniki.

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

Mimo że statyczny konstruktor długo działał, pozostałe wątki zatrzymały się i czekały. Wszystkie wątki odczytują wartość _x ustawioną na dole konstruktora statycznego.

Trade-Ideas Philip
źródło
3

Specyfikacja Common Language Infrastructure gwarantuje, że „inicjalizator typu będzie uruchamiany dokładnie raz dla każdego typu, chyba że zostanie wyraźnie wywołany przez kod użytkownika”. (Sekcja 9.5.3.1.) Jeśli więc nie masz jakiejś zwariowanej IL na luźnym wywołaniu Singleton ::. Cctor bezpośrednio (mało prawdopodobne), twój konstruktor statyczny uruchomi się dokładnie raz przed użyciem typu Singleton, zostanie utworzona tylko jedna instancja Singleton, a twoja właściwość Instancji jest bezpieczna dla wątków.

Należy zauważyć, że jeśli konstruktor Singleton uzyskuje dostęp do właściwości Instancji (nawet pośrednio), wówczas właściwość Instancji będzie miała wartość NULL. Najlepsze, co możesz zrobić, to wykryć, kiedy to nastąpi, i zgłosić wyjątek, sprawdzając, czy instancja nie ma wartości NULL w module dostępu do właściwości. Po zakończeniu konstruktora statycznego właściwość Instancja będzie inna niż null.

Jak wskazuje odpowiedź Zoomba, będziesz musiał zapewnić Singletonowi bezpieczny dostęp z wielu wątków lub zaimplementować mechanizm blokujący za pomocą instancji singletonu.

Dominic Cooney
źródło
2

Po prostu pedantyczny, ale nie ma czegoś takiego jak konstruktor statyczny, ale raczej inicjatory typu statycznego, oto mała demonstracja zależności cyklicznego konstruktora statycznego, która ilustruje ten punkt.

Florian Doyon
źródło
1
Microsoft wydaje się nie zgadzać. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92
0

Chociaż inne odpowiedzi są w większości poprawne, istnieje jeszcze jedno zastrzeżenie dotyczące konstruktorów statycznych.

Zgodnie z § II.10.5.3.3 ras i zakleszczenia na Infrastruktury ECMA-335 wspólny język

Sama inicjacja typu nie spowoduje zakleszczenia, chyba że jakiś kod wywołany z inicjatora typu (bezpośrednio lub pośrednio) wyraźnie wywoła operacje blokowania.

Poniższy kod powoduje zakleszczenie

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

Oryginalnym autorem jest Igor Ostrovsky, zobacz jego post tutaj .

oleksii
źródło