Wzorzec singleton C # bezpieczny dla wątków

79

Mam kilka pytań dotyczących wzorca singleton udokumentowanego tutaj: http://msdn.microsoft.com/en-us/library/ff650316.aspx

Poniższy kod jest wyciągiem z artykułu:

using System;

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new object();

   private Singleton() {}

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

         return instance;
      }
   }
}

W szczególności, czy w powyższym przykładzie zachodzi potrzeba dwukrotnego porównania wystąpienia z wartością null, przed i po blokadzie? Czy to konieczne? Dlaczego nie wykonać blokady najpierw i dokonać porównania?

Czy istnieje problem w uproszczeniu do następujących?

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

         return instance;
      }
   }

Czy wykonanie blokady jest drogie?

Wayne Phipps
źródło
17
Na marginesie, Jon Skeet ma genialny artykuł na temat bezpieczeństwa nici w Singletons: csharpindepth.com/Articles/General/Singleton.aspx
Arran
leniwy statyczny init byłby lepszy ...
Mitch Wheat,
1
Mam też inne przykłady z wyjaśnieniami tutaj: csharpindepth.com/Articles/General/Singleton.aspx
Serge Voloshenko
Dokładnie to samo pytanie tutaj dla świata Javy.
RBT

Odpowiedzi:

133

Wykonanie blokady jest strasznie kosztowne w porównaniu z prostym sprawdzaniem wskaźnika instance != null.

Wzorzec, który tu widzisz, nazywa się blokowaniem z podwójnym sprawdzeniem . Jego celem jest uniknięcie kosztownej operacji zamka, która będzie potrzebna tylko raz (przy pierwszym dostępie do singletona). Implementacja jest taka, ponieważ musi również zapewnić, że po zainicjowaniu singletona nie będzie żadnych błędów wynikających z warunków wyścigu wątków.

Pomyśl o tym w ten sposób: czysty nullczek (bez a lock) gwarantuje, że dostaniesz poprawną użyteczną odpowiedź tylko wtedy, gdy ta odpowiedź brzmi „tak, obiekt jest już skonstruowany”. Ale jeśli odpowiedź brzmi „jeszcze nie skonstruowana”, oznacza to, że nie masz wystarczających informacji, ponieważ tak naprawdę chciałeś wiedzieć, że nie jest jeszcze skonstruowany i żaden inny wątek nie zamierza go wkrótce skonstruować . Więc używasz zewnętrznego sprawdzenia jako bardzo szybkiego testu początkowego i inicjujesz właściwą, wolną od błędów, ale "kosztowną" procedurę (zablokuj, a następnie sprawdź) tylko wtedy, gdy odpowiedź brzmi "nie".

Powyższa implementacja jest wystarczająco dobra w większości przypadków, ale w tym miejscu warto przeczytać artykuł Jona Skeeta na temat singletonów w języku C #, który ocenia również inne alternatywy.

Jon
źródło
1
Dziękuję za informacyjną odpowiedź z przydatnymi linkami. Bardzo cenione.
Wayne Phipps,
Podwójnie sprawdzone blokowanie - link już nie działa.
El Mac
Przepraszam, miałem na myśli tego drugiego.
El Mac,
1
@ElMac: Witryna Skeet nie działa w bankomacie, w odpowiednim czasie zostanie przywrócona. Będę o tym pamiętać i upewnię się, że łącze nadal działa, gdy się pojawi, dzięki.
Jon
3
Ponieważ .NET 4.0 Lazy<T>wykonuje tę pracę po prostu doskonale.
ilyabreev
34

Lazy<T>Wersja:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy
        = new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance
        => lazy.Value;

    private Singleton() { }
}

Wymaga .NET 4 i C # 6.0 (VS2015) lub nowszego.

andasa
źródło
Otrzymuję komunikat „System.MissingMemberException: 'Typ zainicjowany leniwie nie ma konstruktora publicznego, bez parametrów.” „Za pomocą tego kodu na
platformie
@ttugates, masz rację, dzięki. Kod zaktualizowany o wartość fabryczną wywołania zwrotnego dla leniwego obiektu.
andasa
14

Wykonywanie blokady: Dość tanie (nadal droższe niż test zerowy).

Wykonywanie blokady, gdy ma ją inny wątek: Otrzymujesz koszt tego, co jeszcze mają do zrobienia podczas blokowania, dodany do twojego własnego czasu.

Wykonywanie blokady, gdy ma ją inny wątek, a dziesiątki innych wątków również na nią czekają: Okaleczenie.

Ze względu na wydajność zawsze chcesz mieć blokady, których potrzebuje inny wątek, na możliwie najkrótszy czas.

Oczywiście łatwiej jest wnioskować o „szerokich” zamkach niż wąskich, dlatego warto zacząć od nich szerokich i optymalizować w razie potrzeby, ale są przypadki, których uczymy się z doświadczenia i znajomości, gdzie węższy pasuje do wzorca.

(Nawiasem mówiąc, jeśli możesz po prostu użyć private static volatile Singleton instance = new Singleton()lub po prostu nie możesz używać singletonów, ale zamiast tego użyć klasy statycznej, obie są lepsze pod względem tych obaw).

Jon Hanna
źródło
1
Naprawdę podoba mi się twoje myślenie tutaj. To świetny sposób, aby na to spojrzeć. Chciałbym móc przyjąć dwie odpowiedzi lub +5 tej, wielkie dzięki
Wayne Phipps
2
Jedną z konsekwencji, która staje się ważna, gdy nadszedł czas, aby spojrzeć na wydajność, jest różnica między wspólnymi strukturami, w które można uderzyć jednocześnie, a tymi, które będą miały . Czasami nie spodziewamy się, że takie zachowanie będzie się zdarzać często, ale może, więc musimy zablokować (wystarczy jedna awaria, aby wszystko zepsuć). Innym razem wiemy, że wiele wątków naprawdę jednocześnie trafi na te same obiekty. Jeszcze innym razem nie spodziewaliśmy się dużej współbieżności, ale myliliśmy się. Kiedy trzeba poprawić wydajność, priorytet mają te z dużą współbieżnością.
Jon Hanna,
W Twojej alternatywie volatilenie jest to konieczne, ale powinno readonly. Zobacz stackoverflow.com/q/12159698/428724 .
wezten
7

Powodem jest wydajność. Jeśli instance != null(co zawsze będzie miało miejsce z wyjątkiem pierwszego razu), nie ma potrzeby wykonywania kosztownych czynności lock: dwa wątki uzyskujące jednocześnie dostęp do zainicjowanego singletona byłyby niepotrzebnie synchronizowane.

Heinzi
źródło
4

W prawie każdym przypadku (to znaczy we wszystkich przypadkach z wyjątkiem pierwszych) instancenie będzie zerowa. Uzyskanie blokady jest droższe niż zwykłe sprawdzenie, więc jednorazowe sprawdzenie wartości instanceprzed zablokowaniem jest przyjemną i bezpłatną optymalizacją.

Ten wzorzec nazywa się blokowaniem podwójnie sprawdzonym: http://en.wikipedia.org/wiki/Double-checked_locking

Kevin Gosse
źródło
3

Jeffrey Richter zaleca następujące czynności:



    public sealed class Singleton
    {
        private static readonly Object s_lock = new Object();
        private static Singleton instance = null;
    
        private Singleton()
        {
        }
    
        public static Singleton Instance
        {
            get
            {
                if(instance != null) return instance;
                Monitor.Enter(s_lock);
                Singleton temp = new Singleton();
                Interlocked.Exchange(ref instance, temp);
                Monitor.Exit(s_lock);
                return instance;
            }
        }
    }

Yauheni Charniauski
źródło
Czy zmiana instancji nie jest ulotna, robi to samo?
Ε Г И І И О
1

Z chęcią możesz stworzyć bezpieczną dla wątków instancję Singleton, w zależności od potrzeb aplikacji, jest to zwięzły kod, chociaż wolałbym leniwą wersję @ andasa.

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

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}
Brian Ogden
źródło
0

Nazywa się to podwójnie sprawdzonym mechanizmem blokującym, najpierw sprawdzimy, czy instancja została utworzona, czy nie. Jeśli nie, to tylko zsynchronizujemy metodę i utworzymy instancję. Znacząco poprawi to wydajność aplikacji. Wykonywanie blokady jest ciężkie. Aby uniknąć blokady, najpierw musimy sprawdzić wartość null. Jest to również bezpieczne dla wątków i jest najlepszym sposobem na osiągnięcie najlepszej wydajności. Proszę spojrzeć na poniższy kod.

public sealed class Singleton
{
    private static readonly object Instancelock = new object();
    private Singleton()
    {
    }
    private static Singleton instance = null;

    public static Singleton GetInstance
    {
        get
        {
            if (instance == null)
            {
                lock (Instancelock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}
Pranaya Rout
źródło
0

Inna wersja Singleton, w której następujący wiersz kodu tworzy instancję Singleton w momencie uruchamiania aplikacji.

private static readonly Singleton singleInstance = new Singleton();

Tutaj CLR (Common Language Runtime) zajmie się inicjalizacją obiektów i bezpieczeństwem wątków. Oznacza to, że nie będziemy musieli pisać żadnego kodu jawnie do obsługi bezpieczeństwa wątków w środowisku wielowątkowym.

„Zachłanne ładowanie we wzorcu projektowym singleton to nic innego jak proces, w którym musimy zainicjować obiekt singleton w momencie uruchamiania aplikacji, a nie na żądanie i utrzymywać go w pamięci do wykorzystania w przyszłości”.

public sealed class Singleton
    {
        private static int counter = 0;
        private Singleton()
        {
            counter++;
            Console.WriteLine("Counter Value " + counter.ToString());
        }
        private static readonly Singleton singleInstance = new Singleton(); 

        public static Singleton GetInstance
        {
            get
            {
                return singleInstance;
            }
        }
        public void PrintDetails(string message)
        {
            Console.WriteLine(message);
        }
    }

od strony głównej:

static void Main(string[] args)
        {
            Parallel.Invoke(
                () => PrintTeacherDetails(),
                () => PrintStudentdetails()
                );
            Console.ReadLine();
        }
        private static void PrintTeacherDetails()
        {
            Singleton fromTeacher = Singleton.GetInstance;
            fromTeacher.PrintDetails("From Teacher");
        }
        private static void PrintStudentdetails()
        {
            Singleton fromStudent = Singleton.GetInstance;
            fromStudent.PrintDetails("From Student");
        }
Jaydeep Shil
źródło
Niezła alternatywa, ale nie odpowiada na pytanie, które dotyczyło sprawdzania blokowania w konkretnej implementacji, o której mowa w pytaniu
Wayne Phipps
nie bezpośrednio, ale może być używany jako alternatywny „wzorzec singleton C # bezpieczny dla wątków”.
Jaydeep Shil,
0

Odporny na odbicie wzór Singleton:

public sealed class Singleton
{
    public static Singleton Instance => _lazy.Value;
    private static Lazy<Singleton, Func<int>> _lazy { get; }

    static Singleton()
    {
        var i = 0;
        _lazy = new Lazy<Singleton, Func<int>>(() =>
        {
            i++;
            return new Singleton();
        }, () => i);
    }

    private Singleton()
    {
        if (_lazy.Metadata() == 0 || _lazy.IsValueCreated)
            throw new Exception("Singleton creation exception");
    }

    public void Run()
    {
        Console.WriteLine("Singleton called");
    }
}
robert
źródło