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

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

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

    public static Singleton Instance
    {
        get { return instance; }
    }
}
10.08.2008 08:23:55
Это потокобезопасно. Предположим, что несколько потоков хотят получить свойство Instanceодновременно. Один из потоков получит команду сначала запустить инициализатор типа (также известный как статический конструктор). Между тем все остальные потоки, желающие прочитать Instanceсвойство, будут заблокированы до завершения инициализации типа. Только после завершения инициализации поля потокам будет разрешено получить Instanceзначение. Так что никто не может видеть Instanceсущество null.
Jeppe Stig Nielsen 7.07.2014 12:51:46
@JeppeStigNielsen Другие темы не заблокированы. Из собственного опыта я получил из-за этого неприятные ошибки. Гарантия заключается в том, что только первый поток запустит статический инициализатор или конструктор, но тогда другие потоки попытаются использовать статический метод, даже если процесс построения не завершится.
Narvalex 20.08.2018 13:06:25
@Narvalex Этот пример программы (источник, закодированный в URL) не может воспроизвести проблему, которую вы описываете. Может это зависит от того, какая у вас версия CLR?
Jeppe Stig Nielsen 20.08.2018 19:23:21
@JeppeStigNielsen Спасибо, что нашли время. Не могли бы вы объяснить мне, почему здесь поле переопределено?
Narvalex 21.08.2018 12:57:41
@Narvalex С этим кодом верхний регистр Xполучается -1 даже без многопоточности . Это не проблема безопасности потока. Вместо этого инициализатор x = -1запускается первым (он находится на более ранней строке в коде, номер нижней строки). Затем X = GetX()запускается инициализатор , что делает верхний регистр Xравным -1. Затем static C() { ... }запускается «явный» статический конструктор, инициализатор типа , который изменяется только в нижнем регистре x. Итак, после всего этого Mainметод (или Otherметод) может продолжаться и читать в верхнем регистре X. Его значение будет -1, даже с одним потоком.
Jeppe Stig Nielsen 21.08.2018 13:18:11
10 ОТВЕТОВ
РЕШЕНИЕ

Статические конструкторы гарантированно будут запускаться только один раз в домене приложения до создания каких-либо экземпляров класса или доступа к любым статическим членам. 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();
    }
}
187
11.11.2019 14:01:37
Обратите внимание, что если ваш одноэлементный объект является неизменным, использование мьютекса или любого другого механизма синхронизации является излишним и не должно использоваться. Кроме того, я считаю, что приведенный выше пример реализации очень хрупкий :-). Ожидается, что весь код, использующий Singleton.Acquire (), вызовет Singleton.Release (), когда это будет сделано с использованием экземпляра singleton. В противном случае (например, преждевременный возврат, выход из области действия через исключение, забвение вызова Release), при следующем обращении к этому синглтону из другого потока он будет тупиковым в Singleton.Acquire ().
Milan Gardian 25.06.2009 13:16:55
Договорились, хотя я бы пошел дальше. Если ваш синглтон неизменен, использование синглтона является излишним. Просто определите константы. В конечном счете, для правильного использования синглтона разработчики должны знать, что они делают. Как бы хрупка ни была эта реализация, она все же лучше, чем та, которая в вопросе, где эти ошибки проявляются случайно, а не как явно невыпущенный мьютекс.
Zooba 30.06.2009 05:25:38
Одним из способов уменьшить хрупкость метода Release () является использование другого класса с IDisposable в качестве обработчика синхронизации. Когда вы приобретаете синглтон, вы получаете обработчик и можете поместить код, требующий синглтона, в блок using для обработки выпуска.
CodexArcanum 19.08.2010 17:23:09
Для других, которые могут быть отключены этим: Любые члены статического поля с инициализаторами инициализируются до вызова статического конструктора.
Adam W. McKinley 15.12.2011 16:25:21
В наши дни ответ заключается в том, чтобы использовать Lazy<T>- любой, кто использует код, который я первоначально разместил, делает это неправильно (и, честно говоря, это было не так хорошо для начала - 5 лет назад я не был так хорош в этом, как нынешний -я есть :) ).
Zooba 4.02.2014 21:45:49

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

Обратите внимание, что если конструктор Singleton обращается к свойству Instance (даже косвенно), то свойство Instance будет иметь значение null. Лучшее, что вы можете сделать, - это обнаружить, когда это произойдет, и выдать исключение, проверив, что этот экземпляр не равен нулю в методе доступа к свойству. После завершения статического конструктора свойство Instance будет иметь ненулевое значение.

Как указывает ответ Zoomba , вам нужно сделать Singleton безопасным для доступа из нескольких потоков или внедрить механизм блокировки с использованием экземпляра singleton.

3
23.05.2017 11:47:09

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

Из спецификации языка C # :

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

  • Экземпляр класса создан.
  • На любые статические члены класса есть ссылки.

Так что да, вы можете верить, что ваш синглтон будет правильно создан.

Zooba сделал отличное замечание (и за 15 секунд до меня тоже!), Что статический конструктор не гарантирует потокобезопасный общий доступ к синглтону. Это должно быть обработано по-другому.

28
28.01.2020 00:18:49

Статические конструкторы гарантированно срабатывают только один раз для каждого домена приложения, поэтому ваш подход должен быть в порядке. Тем не менее, он функционально не отличается от более сжатой, встроенной версии:

private static readonly Singleton instance = new Singleton();

Безопасность потоков - более важная проблема, когда вы лениво инициализируете вещи.

6
10.08.2008 08:48:33
Эндрю, это не совсем эквивалентно. Не используя статический конструктор, некоторые гарантии того, когда инициализатор будет выполнен, будут потеряны. Пожалуйста, смотрите эти ссылки для более подробного объяснения: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park 10.08.2008 09:01:14
Дерек, я знаком с «оптимизацией» beforefieldinit, но лично я никогда не беспокоюсь об этом.
Andrew Peters 10.08.2008 09:28:36
рабочая ссылка для комментария @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Эта ссылка устарела: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog 8.06.2015 21:59:10

Статический конструктор гарантированно безопасен для потоков. Также ознакомьтесь с обсуждением Singleton на DeveloperZen: http://www.developerzen.com/2007/07/15/whats-wrong-with-this-code-1-discussion/

1
10.08.2008 23:38:26

Хотя все эти ответы дают один и тот же общий ответ, есть одна оговорка.

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

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
85
7.02.2016 18:11:50
typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha 28.10.2011 01:06:46
Я думаю, что это то, что я пытаюсь сделать. Универсальные типы компилируются как отдельные типы в зависимости от того, какие универсальные параметры используются, поэтому статический конструктор может и будет вызываться несколько раз.
Brian Rudolph 3.11.2011 14:56:16
Это верно, когда T имеет тип значения, для ссылочного типа T будет сгенерирован только один универсальный тип
sll 28.11.2011 10:12:32
@sll: Неверно ... См. мое редактирование
Brian Rudolph 15.12.2011 15:59:14
Интересный, но на самом деле статический созвездитель вызван для всех типов, только что попробовал для нескольких ссылочных типов
sll 15.12.2011 16:31:14

Просто чтобы быть педантичным, но нет такого понятия, как статический конструктор, а есть статические инициализаторы типов, вот небольшая демонстрация циклической статической зависимости конструктора, которая иллюстрирует этот момент.

2
7.09.2014 18:56:01
Microsoft, кажется, не согласен. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92 15.07.2015 16:12:00

Вот версия 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. синхронизация
8
23.11.2011 19:34:05
Ленивый, если у класса нет никакой другой несвязанной статики (как consts). В противном случае доступ к любому статическому методу или свойству приведет к созданию экземпляра. Так что я бы не назвал это ленивым.
Schultz9999 11.06.2013 23:26:13

Хотя другие ответы в основном правильные, есть еще одна оговорка со статическими конструкторами.

Как указано в разделе 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();
    }
}

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

0
25.03.2014 12:19:27

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

    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, установленное в нижней части статического конструктора.

4
1.01.2017 06:37:26