Singleton a wielowątkowość

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.

7 thoughts on “Singleton a wielowątkowość”

  1. Jednoczesne tworzenie instancji przez 2 wątki jest większym problemem niż aktualne wywoływanie metod z wielu wątków na raz na rzecz tego samego obiektu? Żaden z przykładów nie pokazuje jak to robić, *chyba, że* interpreter .NET automatycznie blokuje absolutnie wszystkie wywołania metod. Sam fakt, że można zainicjować ten singleton w taki czy inny sposób z wielu wątków nic nie daje. Ponad to, czy możesz wyjaśnić, dlaczego w ostatnim przykładzie tworzysz dwie instancje Singleton(), bo wygląda na to, że korzystasz tylko z jednej(Child.m_Instance)?

  2. Singleton ma w założeniu zapewniać, że w programie zostanie utworzona tylko jedna instancja danej klasy. Z reguły ta instancja istnieje też w szeroko dostępnej przestrzeni nazw. Jeżeli opisujesz singleton, który ma być dodatkowo bezpieczny w środowisku wielowątkowym, zakładam, że w takim razie będę mógł wywoływać jego dowolne metody z dowolnej liczby wątków. W jaki sposób Twoja implementacja zapewnia mi ten komfort i jak wpływa to na wydajność całego systemu? Ostatnia wersja nie blokuje w widoczny sposób, ale CLR i tak musi użyć barier żeby
    zapewnić bezpieczeństwo. Dodatkowo, wersja zawierająca zamek explicite cierpi z powodu znanego
    problemu: http://blogs.msdn.com/brada/archive/2004/05/12/130935.aspx.

  3. Witam,
    Co do ostatniej implementacji to oczywiście błąd wkradł się w post i już go poprawiłem – pisałem po kolei na blogu te implementacje i zapomniałem usunąć jednej deklaracji. Wielkie dzięki za informacje !
    W blogu opisuje wzorzec singleton, który odpowiada za tworzenie instancji klasy. Synchronizacja pozostałych metod to inna bajka(opisana zresztą w poprzednich postach). W końcu nie wszystkie metody trzeba synchronizować. Synchronizacja konkretnych metod zależna jest od tego co one dokładnie robią, do jakich zasobów się odwołują itp.

  4. Zwracam honor 🙂 Trafiłem na Twój blog pierwszy raz i nie widziałem jeszcze reszty artykułów.

  5. A co powiesz na takie rozwiązanie:
    [MethodImpl(MethodImplOptions.Synchronized)]
    public static Singleton Instance()
    {
    if (null == _Singleton)
    {
    _Singleton = new Singleton();
    }
    return _Singleton;
    }
    Małe, zwięzłe i eleganckie 🙂

  6. Fajnie napisane, podoba mi się.

    Jedyne do czego mogę się doczepić to zwroty typu ‘w pełni optymalne’. Coś jest/może być optymalne albo nie. Tym samym nie ma czegoś mniej lub bardziej (czy w pełni) optymalnego. Po jednym z wykładowców mam takie uczulenie 🙂

    A poza tym to wartościowy i wpis i blog! Pozdrawiam.

Leave a Reply

Your email address will not be published.