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?
c#
design-patterns
singleton
Wayne Phipps
źródło
źródło
Odpowiedzi:
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
null
czek (bez alock
) 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.
źródło
Lazy<T>
wykonuje tę pracę po prostu doskonale.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.
źródło
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).źródło
volatile
nie jest to konieczne, ale powinnoreadonly
. Zobacz stackoverflow.com/q/12159698/428724 .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ścilock
: dwa wątki uzyskujące jednocześnie dostęp do zainicjowanego singletona byłyby niepotrzebnie synchronizowane.źródło
W prawie każdym przypadku (to znaczy we wszystkich przypadkach z wyjątkiem pierwszych)
instance
nie będzie zerowa. Uzyskanie blokady jest droższe niż zwykłe sprawdzenie, więc jednorazowe sprawdzenie wartościinstance
przed zablokowaniem jest przyjemną i bezpłatną optymalizacją.Ten wzorzec nazywa się blokowaniem podwójnie sprawdzonym: http://en.wikipedia.org/wiki/Double-checked_locking
źródło
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; } } }
źródło
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; } }
źródło
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; } } }
źródło
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.
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"); }
źródło
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"); } }
źródło