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

247

Другими словами, безопасен ли этот поток реализации Singleton:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

    public static Singleton Instance
    {
        get { return instance; }
    }
}
urini
источник
1
Это потокобезопасно. Предположим, что несколько потоков хотят получить свойство Instanceодновременно. Один из потоков получит команду сначала запустить инициализатор типа (также известный как статический конструктор). Между тем все остальные потоки, желающие прочитать Instanceсвойство, будут заблокированы до завершения инициализатора типа. Только после завершения инициализации поля потокам будет разрешено получить Instanceзначение. Так что никто не может видеть Instanceбытие null.
Джеппе Стиг Нильсен
@JeppeStigNielsen Другие темы не заблокированы. Из собственного опыта я получил из-за этого неприятные ошибки. Гарантия состоит в том, что только первый поток запустит статический инициализатор или конструктор, но тогда другие потоки попытаются использовать статический метод, даже если процесс построения не завершился.
Narvalex
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();
    }
}
Zooba
источник
53
Обратите внимание, что если ваш одноэлементный объект является неизменным, использование мьютекса или любого другого механизма синхронизации является излишним и не должно использоваться. Кроме того, я считаю приведенный выше пример реализации чрезвычайно хрупким :-). Ожидается, что весь код, использующий Singleton.Acquire (), вызовет Singleton.Release (), когда это будет сделано с использованием экземпляра singleton. В противном случае (например, преждевременный возврат, выход из области действия через исключение, забвение вызова Release), при следующем обращении к этому синглтону из другого потока он будет тупиковым в Singleton.Acquire ().
Милан Гардиан
2
Договорились, хотя я бы пошел дальше. Если ваш синглтон неизменен, использование синглтона является излишним. Просто определите константы. В конечном счете, для правильного использования синглтона разработчики должны знать, что они делают. Как бы хрупка ни была эта реализация, она все же лучше, чем та, которая в вопросе, где эти ошибки проявляются случайно, а не как явно невыпущенный мьютекс.
Zooba
26
Одним из способов уменьшить хрупкость метода Release () является использование другого класса с IDisposable в качестве обработчика синхронизации. Приобретая синглтон, вы получаете обработчик и можете поместить код, требующий синглтона, в блок using для обработки выпуска.
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 >
Дерек Парк
Дерек, я знаком с «оптимизацией» beforefieldinit, но лично я никогда не беспокоюсь об этом.
Эндрю Питерс
рабочая ссылка для комментария @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Эта ссылка устарела: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog,
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

Спецификация инфраструктуры общего языка гарантирует, что «инициализатор типа должен запускаться ровно один раз для любого данного типа, если это явно не вызвано кодом пользователя». (Раздел 9.5.3.1.) Таким образом, если у вас нет какого-то дурацкого IL на вызове Singleton ::. Cctor напрямую (маловероятно), ваш статический конструктор будет запущен ровно один раз перед использованием типа Singleton, будет создан только один экземпляр Singleton, и ваше свойство Instance является поточно-ориентированным.

Обратите внимание, что если конструктор Singleton обращается к свойству Instance (даже косвенно), свойство Instance будет иметь значение null. Лучшее, что вы можете сделать, это обнаружить, когда это происходит, и выдать исключение, проверив, что экземпляр не равен NULL в методе доступа к свойству. После завершения статического конструктора свойство 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();
    }
}

Оригинальный автор Игорь Островский, смотрите его пост здесь .

алексей
источник