Чи безпечна нитка статичного конструктора C #?


247

Іншими словами, чи безпечний цей потік реалізації Singleton:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

    static Singleton()
    {
        instance = new Singleton();
    }

    public static Singleton Instance
    {
        get { return instance; }
    }
}

1
Це безпечно для ниток. Припустимо, декілька потоків хочуть отримати властивість Instanceодразу. Одному з потоків буде запропоновано спочатку запустити ініціалізатор типу (також відомий як статичний конструктор). Тим часом всі інші потоки, які бажають прочитати Instanceвластивість, будуть заблоковані, поки ініціалізатор типу не закінчиться. Лише після того, як ініціалізатор поля завершиться, потокам буде дозволено отримати Instanceзначення. Так що ніхто не може бачити Instanceістота null.
Jeppe Stig Nielsen

@JeppeStigNielsen Інші теми не заблоковані. З власного досвіду у мене виникли неприємні помилки через це. Гарантія полягає в тому, що тільки кулакова нитка запустить статичний ініціалізатор або конструктор, але тоді інші потоки спробують використовувати статичний метод, навіть якщо процес будівництва не закінчився.
Нарвалекс

2
@Narvalex Ця зразкова програма (джерело, закодоване в URL) не може відтворити описану вами проблему. Може, це залежить від того, яку версію CLR у вас є?
Джеппе Стіг Нільсен

@JeppeStigNielsen Дякуємо, що знайшли ваш час. Чи можете ви мені поясніть, чому тут поле перекрито?
Narvalex

5
@Narvalex З цим кодом великі регістри Xзакінчуються -1 навіть без нарізки . Це не питання безпеки потоку. Натомість ініціалізатор x = -1запускається спочатку (він знаходиться на попередньому рядку в коді, нижній номер рядка). Потім X = GetX()запускається ініціалізатор , що робить верхній регістр Xрівним -1. І тоді «явний» статичний конструктор static C() { ... }запускає ініціалізатор типу , який змінюється лише малі регістри x. Тож після цього Mainметод (або Otherметод) може продовжувати і читати великі регістри X. Його значення буде -1навіть з однією ниткою.
Джеппе Стіг Нільсен

Відповіді:


189

Гарантовано, що статичні конструктори будуть запущені лише один раз у домені програми, перш ніж створюватись будь-які екземпляри класу або мати доступ до будь-яких статичних членів. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

Показана реалізація є потоковою безпекою для початкової побудови, тобто для конструювання об'єкта Singleton не потрібно блокування або тестування нуля. Однак це не означає, що будь-яке використання екземпляра буде синхронізовано. Існують різноманітні способи, як це можна зробити; Я показав один нижче.

public class Singleton
{
    private static Singleton instance;
    // Added a static mutex for synchronising use of instance.
    private static System.Threading.Mutex mutex;
    private Singleton() { }
    static Singleton()
    {
        instance = new Singleton();
        mutex = new System.Threading.Mutex();
    }

    public static Singleton Acquire()
    {
        mutex.WaitOne();
        return instance;
    }

    // Each call to Acquire() requires a call to Release()
    public static void Release()
    {
        mutex.ReleaseMutex();
    }
}

53
Зауважте, що якщо ваш одиночний об'єкт є незмінним, використання mutex або будь-якого механізму синхронізації є надмірним вмістом, і його не слід використовувати. Крім того, я вважаю, що вищевказана реалізація зразка є надзвичайно крихкою :-). Очікується, що весь код, що використовує Singleton.Acquire (), викликає Singleton.Release (), коли це робиться за допомогою екземпляра singleton. Якщо цього не зробити (наприклад, передчасно повернутися, залишити область за допомогою винятку, забувши зателефонувати Release), наступного разу, коли цей Singleton буде доступний з іншого потоку, він зайде в тупик в Singleton.Acquire ().
Мілан Гардіян,

2
Домовились, хоча я б пішов далі. Якщо ваш однолінійний непорушний, використання однолітка є надмірним. Просто визначте константи. Зрештою, правильне використання одинарного вимагає, щоб розробники знали, що вони роблять. Настільки крихкою, як ця реалізація, вона все-таки краща за ту, у якій йдеться про питання, де ці помилки виявляються випадковим чином, а не як явно невідомий мютекс.
Zooba

26
Одним із способів зменшити крихкість методу Release () є використання іншого класу з IDisposable як обробник синхронізації. Коли ви придбаєте синглтон, ви отримаєте обробник і зможете помістити код, який вимагає синглтона, у блок використання для обробки випуску.
CodexArcanum

5
Для інших, які можуть зіткнутися цим: будь-які члени статичного поля з ініціалізаторами ініціалізуються перед викликом статичного конструктора.
Адам У. Мак-Кінлі

12
Відповідь в наші дні полягає у використанні Lazy<T>- кожен, хто використовує код, який я спочатку розмістив, робить це неправильно (і, чесно кажучи, це було не так добре для початку - 5 років тому - мені було не так добре в цьому матеріалі, як нинішній -ме це :)).
Zooba

86

Хоча всі ці відповіді дають однакову загальну відповідь, є один застереження.

Пам'ятайте, що всі потенційні похідні родового класу складаються як окремі типи. Тому будьте обережні, застосовуючи статичні конструктори для загальних типів.

class MyObject<T>
{
    static MyObject() 
    {
       //this code will get executed for each T.
    }
}

Редагувати:

Ось демонстрація:

static void Main(string[] args)
{
    var obj = new Foo<object>();
    var obj2 = new Foo<string>();
}

public class Foo<T>
{
    static Foo()
    {
         System.Diagnostics.Debug.WriteLine(String.Format("Hit {0}", typeof(T).ToString()));        
    }
}

У консолі:

Hit System.Object
Hit System.String

typeof (MyObject <T>)! = typeof (MyObject <Y>);
Карим Ага

6
Я думаю, що це я намагаюся зробити. Загальні типи складаються як окремі типи, на основі яких використовуються загальні параметри, тому статичний конструктор може і буде називатися кілька разів.
Брайан Рудольф

1
Це правильно, коли T є значенням типу, для еталонного типу T генерується лише один загальний тип
sll

2
@sll: Неправда ... Дивіться мою редагування
Брайан Рудольф

2
Цікавий, але по-справжньому статичний контруктор закликав усі типи, просто спробував для декількох типів посилань
sll

28

Використання статичного конструктора насправді є безпечним для потоків. Статичний конструктор гарантовано виконується лише один раз.

Із специфікації мови C # :

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

  • Створюється екземпляр класу.
  • Будь-який із статичних членів класу посилається.

Так що так, ви можете довіряти, що ваш одиночний код буде правильно встановлений.

Zooba висловив чудову думку (і за 15 секунд до мене теж!), Що статичний конструктор не гарантуватиме безпечний для потоків спільний доступ до одиночного. З цим потрібно вирішити іншим способом.


8

Ось версія Cliffnotes з наведеної вище сторінки MSDN на c # singleton:

Завжди використовуйте таку схему, ви не можете помилитися:

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

   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

Окрім очевидних функцій сингтона, він надає вам ці дві речі безкоштовно (стосовно сингтона в c ++):

  1. ліниве будівництво (або ніяке будівництво, якщо його ніколи не називали)
  2. синхронізація

3
Ледачий, якщо у класу немає жодної іншої неспорідненої статики (наприклад, consts). Інакше доступ до будь-якого статичного методу чи властивості призведе до створення екземпляра. Тож я б не назвав це ледачим.
Schultz9999

6

Статичні конструктори гарантовано спрацьовують лише один раз у домені програми, тому ваш підхід повинен бути нормальним. Однак він функціонально не відрізняється від більш стислої, вбудованої версії:

private static readonly Singleton instance = new Singleton();

Безпека ниток є більшою проблемою, коли ви лінь ініціалізуєте речі.


4
Ендрю, це не зовсім рівнозначно. Якщо не використовувати статичний конструктор, деякі гарантії щодо того, коли буде виконаний ініціалізатор, втрачаються. Будь ласка, перегляньте ці посилання для поглибленого пояснення: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Дерек, я знайомий з beforefieldinit «оптимізації» , але, особисто я ніколи не турбуватися про це.
Ендрю Пітерс

робоча посилання на коментар @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Здається, це посилання є несвіжим: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
голос

4

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

    private class InitializerTest
    {
        static private int _x;
        static public string Status()
        {
            return "_x = " + _x;
        }
        static InitializerTest()
        {
            System.Diagnostics.Debug.WriteLine("InitializerTest() starting.");
            _x = 1;
            Thread.Sleep(3000);
            _x = 2;
            System.Diagnostics.Debug.WriteLine("InitializerTest() finished.");
        }
    }

    private void ClassInitializerInThread()
    {
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() starting.");
        string status = InitializerTest.Status();
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() status = " + status);
    }

    private void classInitializerButton_Click(object sender, EventArgs e)
    {
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
    }

Код, наведений вище, дав результати нижче.

10: ClassInitializerInThread() starting.
11: ClassInitializerInThread() starting.
12: ClassInitializerInThread() starting.
InitializerTest() starting.
InitializerTest() finished.
11: ClassInitializerInThread() status = _x = 2
The thread 0x2650 has exited with code 0 (0x0).
10: ClassInitializerInThread() status = _x = 2
The thread 0x1f50 has exited with code 0 (0x0).
12: ClassInitializerInThread() status = _x = 2
The thread 0x73c has exited with code 0 (0x0).

Навіть незважаючи на те, що статичний конструктор зайняв багато часу, інші потоки зупинилися і чекали. Усі потоки зчитують значення _x, встановлене внизу статичного конструктора.


3

Специфікації Common Language Infrastructure гарантує , що «тип ініціалізатор повинен працювати рівно один раз для будь-якого даного типу, якщо явно не викликається кодом користувача.» (Розділ 9.5.3.1.) Отже, якщо у вас є якийсь хитрий IL на вільному виклику Singleton ::. Cctor безпосередньо (малоймовірно), ваш статичний конструктор запуститься рівно один раз до використання типу Singleton, буде створено лише один екземпляр Singleton, і ваше властивість Instance захищено потоками.

Зауважте, що якщо конструктор Singleton звертається до властивості Instance (навіть опосередковано), то властивість Instance буде нульовою. Найкраще, що ви можете зробити, це виявити, коли це трапиться, і викинути виняток, перевіривши, що цей примірник не має значення в доступі до ресурсу. Після того, як ваш статичний конструктор завершить, властивість Instance буде недійсною.

Як вказує відповідь Zoomba , вам потрібно буде зробити Singleton безпечним для доступу з декількох потоків або застосувати механізм блокування навколо, використовуючи екземпляр singleton.


2

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


1
Microsoft, здається, не погоджується. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92


0

Хоча інші відповіді в основному правильні, є ще один застереження зі статичними конструкторами.

Як зазначено в розділі II.10.5.3.3 Раси і тупиків в інфраструктурі ECMA-335 Common Language

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

Наступний код призводить до тупикової ситуації

using System.Threading;
class MyClass
{
    static void Main() { /* Won’t run... the static constructor deadlocks */  }

    static MyClass()
    {
        Thread thread = new Thread(arg => { });
        thread.Start();
        thread.Join();
    }
}

Оригінальний автор - Ігор Островський, дивіться його пост тут .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.