Z racji tego, że w ostatnim czasie sporo pisałem o wielowątkowości w C#, dzisiaj pokaże prawidłową implementacje wzorca projektowego singleton przystosowanego do pracy w środowisku współbieżnym. Na początek przyjrzyjmy się klasycznej implementacji:
public sealed class Singleton { private static Singleton m_Instance = null; private Singleton() { } public static Singleton Instance { get { if(m_Instance == null) m_Instance = new Signleton(); return m_Instance; } } }
Wyobraźmy sobie sytuację w której dwa wątki jednocześnie próbują utworzyć instancję klasy. Może zdarzyć się, że wątki jednocześnie wejdą do if’a sprawdzającego m_Instance. W takim przypadku zostaną utworzone dwie instancje Signleton. Jak temu zaradzić?
Wystarczy użyć jednego z mechanizmów synchronizacji opisanych we wcześniejszych postach. Polecam lock – w przypadku synchronizacji wewnątrz AppDomain jest to przeważnie najlepsze rozwiązanie.
public sealed class Singleton { private static Singleton m_Instance = null; private static readonly object m_Sync = new object(); private Singleton() { } public static Singleton Instance { get { lock(m_Sync) { if(m_Instance == null) m_Instance = new Signleton(); } return m_Instance; } } }
Powyższa implementacja jest poprawna, jednak ma jedną wadę – za każdym razem trzeba zakładać blokadę co w środowisku rozproszonym może spowodować istotny spadek wydajności. Spróbujmy więc zakładać blokadę wyłącznie gdy m_Instance jest równy null (czyli podczas pierwszego wywołania właściwości Instance):
public sealed class Singleton { private static Singleton m_Instance = null; private static readonly object m_Sync = new object(); private Singleton() { } public static Singleton Instance { get { if(m_Instance == null ) { lock(m_Sync) { if(m_Instance == null) m_Instance = new Signleton(); } } return m_Instance; } } }
Z kodu widzimy, że synchronizujemy kod tylko gdy instancja nie została jeszcze utworzona. Po zainicjowaniu obiektu kod działa już w pełni optymalnie, zwracając po prostu obiekt Singleton.
Jeśli nie zależy nam na tzw. lazy loading(opóźnione ładowanie) możemy stworzyć instancję obiektu w momencie deklaracji m_Instance:
public sealed class Singleton { private static Singleton m_Instance = new Singleton(); private Singleton() { } public static Singleton Instance { get { return m_Instance; } } }
Implementacja jest w pełni optymalna oraz bezpieczna w środowisku współbieżnym. Jedyną wadą jest fakt, że Signleton będzie utworzony(podczas np. dostępu do innych statycznych pól klasy) nawet w przypadku gdy z niego nie będziemy korzystać w programie. Jeśli wiemy, że za każdym razem podczas działania aplikacji będziemy wywoływać klasę to powyższe rozwiązanie może okazać się bardzo dobre.
Czas na najlepsze moim zdaniem rozwiązanie:
public sealed class Singleton { private Singleton() { } public static Singleton Instance { get { return Child.m_Instance; } } public static int TestValue { get; set; } class Child { private Child() { } internal static readonly Singleton m_Instance = new Singleton(); } }
Przedstawiona implementacja jest optymalna ponieważ nie zakładamy żadnych blokad. Ponadto jest w pełni “leniwa”- inicjujemy obiekt dopiero gdy chcemy mieć do niego dostęp. Jeśli przypiszemy jakąś wartość właściwości TestValue, obiekt Singleton nie zostanie utworzony – w przypadku wcześniejszej implementacji jakikolwiek dostęp do statycznych właściwośći powodował inicjalizację Singleton. Rozwiązanie jest oczywiście bezpieczne w środowisku współbieżnym.