Нитка Safe C # Singleton Pattern


79

У мене є декілька запитань щодо синглтон-шаблону, як задокументовано тут: http://msdn.microsoft.com/en-us/library/ff650316.aspx

Наступний код - це витяг із статті:

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

Зокрема, у наведеному вище прикладі, чи потрібно двічі порівнювати екземпляр із нулем, до і після блокування? Це необхідно? Чому б не виконати блокування спочатку і не провести порівняння?

Чи існує проблема у спрощенні до наступного?

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

         return instance;
      }
   }

Виконання блокування дороге?


17
Окрім того, Джон Скіт має блискучу статтю про безпеку ниток у Singletons: csharpindepth.com/Articles/General/Singleton.aspx
Arran,

лінивий статичний ініт буде кращим ...
Mitch Wheat

1
Я також отримав тут інші приклади з поясненнями: csharpindepth.com/Articles/General/Singleton.aspx
Серж Волошенко

Точно таке ж питання тут і для світу Java.
RBT

Відповіді:


133

Виконання блокування надзвичайно дороге порівняно з простою перевіркою вказівника instance != null.

Шаблон, який ви бачите тут, називається двічі перевіреною фіксацією . Його метою є уникнення дорогої операції блокування, яка буде потрібна лише один раз (при першому доступі до синглтона). Реалізація є такою, оскільки вона також повинна забезпечити, щоб при ініціалізації одиночного не було помилок, що виникають внаслідок перегонів потоків.

Подумайте про це так: гола nullперевірка (без a lock) гарантовано дасть вам правильну корисну відповідь лише тоді, коли ця відповідь "так, об'єкт вже побудований". Але якщо відповідь "ще не побудована", у вас недостатньо інформації, тому що ви дійсно хотіли знати, це те, що вона "ще не побудована, і жоден інший потік не збирається її створювати найближчим часом ". Отже, ви використовуєте зовнішню перевірку як дуже швидкий початковий тест і ініціюєте правильну, без помилок, але "дорогу" процедуру (блокування, а потім перевірка), лише якщо відповідь "ні".

Вищевказана реалізація є достатньою для більшості випадків, але на цьому етапі непогано піти і прочитати статтю Джона Скіта про одиночні в C #, яка також оцінює інші альтернативи.


1
Дякую за інформативну відповідь із корисними посиланнями. Цінується.
Уейн Фіппс,

Перевірена блокування - посилання більше не працює.
El Mac,

Вибачте, я мав на увазі іншого.
El Mac,

1
@ElMac: Веб-сайт Skeet не працює в банкоматі, він буде створений з часом. Я пам’ятаю про це та переконуюсь, що посилання все ще працює, коли воно з’явиться, дякую.
Джон

3
Оскільки .NET 4.0 Lazy<T>робить цю роботу просто ідеально.
ilyabreev

34

Lazy<T>версія:

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

    public static Singleton Instance
        => lazy.Value;

    private Singleton() { }
}

Потрібна .NET 4 та C # 6.0 (VS2015) або новіша версія.


Я отримую "System.MissingMemberException: 'Ліниво ініціалізований тип не має загальнодоступного конструктора без параметрів.'" З цим кодом у .Net 4.6.1 / C # 6.
ttugates

@ttugates, ти маєш рацію, дякую. Код оновлений із фабричним зворотним викликом для ледачого об’єкта.
andasa

14

Виконання блокування: Досить дешево (все ще дорожче, ніж нульовий тест).

Виконання блокування, коли його має інший потік: Ви отримуєте витрати на все, що їм ще потрібно робити під час блокування, додані до вашого власного часу.

Виконання блокування, коли його має інший потік, і на нього також чекають десятки інших потоків: калікування.

З міркувань продуктивності ви завжди хочете мати замки, яких хоче інший потік, на найкоротший проміжок часу.

Звичайно, про «широкі» замки легше міркувати, аніж вузькі, тому варто починати з них широкі та оптимізувати за потребою, але є деякі випадки, про які ми дізнаємось із досвіду та знайомих, коли більш вузьке підходить під шаблон.

(До речі, якщо ви можете просто використовувати private static volatile Singleton instance = new Singleton()або якщо ви можете просто не використовувати однотонні, а використовувати статичний клас замість цього, обидва краще з точки зору цих проблем).


1
Мені дуже подобається ваше мислення тут. Це чудовий спосіб на це подивитися. Я хотів би прийняти дві відповіді або +5 до цієї, велике спасибі
Уейн Фіппс,

2
Одним із наслідків, який стає важливим, коли приходить час дивитись на продуктивність, є різниця між спільними структурами, які можуть бути вражені одночасно, і тими, які будуть . Іноді ми не очікуємо, що така поведінка траплятиметься часто, але це могло б статися, тому нам потрібно заблокувати (потрібно лише одна помилка блокування, щоб все зіпсувати). В інших випадках ми знаємо, що безліч потоків дійсно будуть одночасно потрапляти на ті самі об’єкти. Однак інший раз ми не очікували, що буде багато збігів, але ми помилялися. Коли потрібно покращити продуктивність, пріоритет мають ті, у кого багато паралелізму.
Джон Ханна,

З іншого боку, volatileце не потрібно, однак воно повинно бути readonly. Див. Stackoverflow.com/q/12159698/428724 .
wezten

7

Причина - продуктивність. Якщо instance != null(що завжди буде так, крім першого разу), немає необхідності робити дорого lock: два потоки, що одночасно отримують доступ до ініціалізованого одиночного, будуть синхронізовані без необхідності.


4

Майже в кожному випадку (тобто: у всіх випадках, крім найпершого), instanceне буде нулем. Придбання блокування дорожче, ніж проста перевірка, тому перевірка одного разу значення instanceперед блокуванням є приємною та безкоштовною оптимізацією.

Цей шаблон називається двічі перевіреною блокуванням: http://en.wikipedia.org/wiki/Double- Check_Locking


3

Джеффрі Ріхтер рекомендує наступне:



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


Чи не робить змінну екземпляра нестійкою, чи не те саме?
Ε Г И І І О О

1

Ви можете охоче створити безпечний для потоку екземпляр Singleton, залежно від потреб вашої програми, це стислий код, хоча я б віддав перевагу лінивій версії @ andasa.

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

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}

0

Це називається Механізм подвійної перевірки, спочатку ми перевіримо, створений екземпляр чи ні. Якщо ні, тоді лише ми синхронізуємо метод і створимо екземпляр. Це суттєво покращить продуктивність програми. Виконання блокування важке. Отже, щоб уникнути блокування спочатку нам потрібно перевірити нульове значення. Це також безпечно для потоків, і це найкращий спосіб досягти найкращої продуктивності. Будь ласка, подивіться на наступний код.

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

0

Інша версія Singleton, де наступний рядок коду створює екземпляр Singleton під час запуску програми.

private static readonly Singleton singleInstance = new Singleton();

Тут CLR (Common Language Runtime) подбає про ініціалізацію об’єктів та безпеку потоків. Це означає, що ми не будемо вимагати писати будь-який код явно для роботи з безпекою потоків для багатопоточного середовища.

"Стрімке завантаження в шаблоні однотонне проектування - це не процес, при якому нам потрібно ініціалізувати одномісний об’єкт під час запуску програми, а не на вимогу, і тримати його готовим у пам’яті для подальшого використання".

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

від основного:

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

Хороша альтернатива, але не дає відповіді на запитання про перевірку блокування в конкретній реалізації, згаданій у питанні
Уейн Фіппс,

не безпосередньо, але може бути використаний як альтернатива "Шаблон однотонній схеми C #".
Jaydeep Shil

0

Стійкий до відбиття шаблон 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");
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.