Является ли статический конструктор C# потокобезопасным?

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

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

Это потокобезопасный. Предположим, несколько потоков хотят получить свойство Instance сразу. Одному из потоков будет предложено сначала запустить инициализатор типа (также известный как статический конструктор). Между тем, все другие потоки, желающие прочитать свойство Instance, будут заперто, пока инициализатор типа не завершит работу. Только после завершения инициализатора поля потокам будет разрешено получать значение Instance. Таким образом, никто не видит, что Instance является null.

Jeppe Stig Nielsen 07.07.2014 16:51

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

Narvalex 20.08.2018 16:06

@Narvalex Эта программа-пример (источник, закодированный в URL) не может воспроизвести описанную вами проблему. Может это зависит от того, какая у вас версия CLR?

Jeppe Stig Nielsen 20.08.2018 22:23

@JeppeStigNielsen Спасибо, что нашли время. Не могли бы вы объяснить мне, почему поле здесь переопределено?

Narvalex 21.08.2018 15:57

@Narvalex С этим кодом X в верхнем регистре становится -1даже без заправки. Это не проблема безопасности потоков. Вместо этого инициализатор x = -1 запускается первым (он находится в более ранней строке кода, под нижним номером строки). Затем запускается инициализатор X = GetX(), который делает X в верхнем регистре равным -1. Затем запускается «явный» статический конструктор, инициализатор типа static C() { ... }, который изменяет только строчные x. Итак, после всего этого, метод Main (или метод Other) может продолжить чтение X в верхнем регистре. Его значение будет -1, даже с одним потоком.

Jeppe Stig Nielsen 21.08.2018 16:18
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
251
5
63 474
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

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

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

Ответ принят как подходящий

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

Показанная реализация является потокобезопасной для начальной конструкции, то есть для создания объекта Singleton не требуется блокировка или проверка на null. Однако это не означает, что любое использование экземпляра будет синхронизировано. Это можно сделать разными способами; Я показал один ниже.

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

Обратите внимание: если ваш одноэлементный объект неизменен, использование мьютекса или любого механизма синхронизации является излишним и не должно использоваться. Кроме того, я считаю приведенный выше пример реализации чрезвычайно хрупким :-). Ожидается, что весь код, использующий Singleton.Acquire (), будет вызывать Singleton.Release (), когда это делается с использованием экземпляра singleton. Если этого не сделать (например, преждевременно вернуться, выйти из области действия через исключение, забыть вызвать Release), в следующий раз, когда к этому синглтону будет обращаться из другого потока, он зайдет в тупик в Singleton.Acquire ().

Milan Gardian 25.06.2009 17:16

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

Zooba 30.06.2009 09:25

Один из способов уменьшить хрупкость метода Release () - использовать другой класс с IDisposable в качестве обработчика синхронизации. Когда вы приобретаете синглтон, вы получаете обработчик и можете поместить код, требующий синглтона, в блок using для обработки выпуска.

CodexArcanum 19.08.2010 21:23

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

Adam W. McKinley 15.12.2011 20:25

Простой потокобезопасный одноэлементный бланк для повседневного использования: ilyatereschuk.blogspot.com/2013/12/…

user2604650 24.12.2013 10:53

@ user2604650 В этом нет ничего поточно-ориентированного, кроме создания экземпляра синглтона, который уже был бы, если бы он был инициализирован в статическом конструкторе. Лично я думаю, что использование такого шаблона для синглтона было бы излишним, поскольку в большинстве случаев единственный раз, когда вы обращаетесь к синглтону, будет во время доступа к свойству статического экземпляра, что в результате является временем вызова статического конструктора.

Aidiakapi 28.01.2014 20:55

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

Zooba 05.02.2014 01:45

Зачем использовать здесь Mutex? Метод Acquire просто возвращает ссылку на экземпляр (это ссылочный тип). Не имеет значения, является ли объект, на который имеется ссылка, изменяемым или неизменным. Ссылка относится к одному и тому же объекту независимо от того, мутирует ли объект. Зачем делать блокировку? Даже если бы экземпляр не был «одноэлементным» или «только для чтения», т.е. даже если бы ссылка на поле могла измениться, начиная с ссылочные присвоения являются атомарными, все равно не было бы необходимости в блокировке / Mutex. Я проголосую против этого ответа @ +109 чистых голосов за ...

Jeppe Stig Nielsen 07.07.2014 18:57

@JeppeStigNielsen Идея состоит в том, что вы сохраняете мьютекс, пока используете объект, а затем отпускаете его, когда закончите. Опять же, есть более эффективные способы сделать это в зависимости от того, сколько накладных расходов вы хотите возложить на своих пользователей. Сейчас я больше поклонник шаблона IDisposable / using (), но концепция та же.

Zooba 23.07.2014 20:59

@Zooba нет ничего, что могло бы помешать вызывающему абоненту сохранить ссылку на Singleton после вызова Release: var s = Singleton.Acquire(); Singleton.Release(); s.Property = value;

phoog 09.06.2015 00:48

@phoog Да, плохие парни все равно могут победить, если они могут запустить произвольный код в вашем приложении. Если вы не доверяете своим коллегам-разработчикам писать хороший код, у вас возникают проблемы, которые синглтон не может решить.

Zooba 10.06.2015 01:58

@Zooba Я не доверяю себе писать хороший код, поэтому стараюсь избегать требований вроде «если вы позвоните так, то вам придется вызвать позже» или «вы не сможете использовать это после того, как позвоните тому». Я не беспокоюсь о плохих парнях, просто о человеческих ошибках. Конечно, тестирование должно уловить это, но зачем полагаться на тестирование, если почти всегда можно создать более чистый дизайн, менее уязвимый для человеческих ошибок?

phoog 10.06.2015 02:53

@phoog Справедливо, но я бы отнес "синглтоны, применяемые компилятором" к категории вещей, требующих более чистого дизайна. Создание более строго принудительного синглтона не поможет вам, когда проблема в том, что у вас не должно быть синглтона. (Неудивительно, что мои взгляды изменились за 7 лет с тех пор, как я написал этот ответ, не то чтобы я когда-либо считал это действительно хорошей идеей :))

Zooba 11.06.2015 00:01

@Zooba именно так. Я не учел мудрости (или ее отсутствия) общего дизайна.

phoog 11.06.2015 00:04

@ AdamW.McKinley - обратите внимание, что порядок статической инициализации отличается в версиях до .NET 4.0 и 4.0. codeblog.jonskeet.uk/2010/01/26/…

Dave Black 02.02.2016 01:51

@Zooba Не могли бы вы пояснить, что означает «запускать только один раз на домен приложения»? Что произойдет, если я попытаюсь вызвать это из своего приложения службы Windows, а также из веб-приложения?

OmGanesh 30.01.2019 19:36

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

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

The static constructor for a class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

  • An instance of the class is created.
  • Any of the static members of the class are referenced.

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

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

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

private static readonly Singleton instance = new Singleton();

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

Эндрю, это не полностью эквивалентно. Если не использовать статический конструктор, теряются некоторые гарантии того, когда инициализатор будет выполнен. Пожалуйста, просмотрите эти ссылки для более подробного объяснения: * csharpindepth.com/Articles/General/Beforefieldinit.aspx> * ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html>

Derek Park 10.08.2008 13:01

Дерек, я знаком с «оптимизацией» до поля, но лично меня это не волнует.

Andrew Peters 10.08.2008 13:28

рабочая ссылка на комментарий @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx. Эта ссылка устарела: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html

phoog 09.06.2015 00:59

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

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

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

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)! = typeof (MyObject);

Karim Agha 28.10.2011 05:06

Я думаю, что это то, что я пытаюсь донести. Универсальные типы компилируются как отдельные типы в зависимости от того, какие общие параметры используются, поэтому статический конструктор может и будет вызываться несколько раз.

Brian Rudolph 03.11.2011 18:56

Это правильно, когда T имеет тип значения, для ссылочного типа T будет сгенерирован только один универсальный тип

sll 28.11.2011 14:12

@sll: Неправда ... См. мое редактирование

Brian Rudolph 15.12.2011 19:59

См. Дженерики в среде выполненияWhen a generic type is first constructed with a value type as a parameter, the runtime creates a specialized generic type with the supplied parameter or parameters substituted in the appropriate places in the MSIL. Specialized generic types are created once for each unique value type used as a parameter

sll 15.12.2011 20:27

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

sll 15.12.2011 20:31

@Brian Rudolph: общим является определение статического конструктора, взгляните на этот код

user1416420 15.09.2013 03:53

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

Microsoft, похоже, не согласен. msdn.microsoft.com/en-us/library/k9x6w0hc.aspx

Westy92 15.07.2015 19:12

Вот версия Cliffnotes с указанной выше страницы MSDN на синглтоне C#:

Всегда используйте следующий шаблон, вы не ошибетесь:

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

   private Singleton(){}

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

Помимо очевидных функций синглтона, он дает вам эти две вещи бесплатно (относительно синглтона в C++):

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

Ленивый, если у класса нет другой несвязанной статики (например, const). В противном случае доступ к любому статическому методу или свойству приведет к созданию экземпляра. Так что я бы не назвал это ленивым.

Schultz9999 12.06.2013 03:26

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

Согласно разделу II.10.5.3.3 Гонки и тупики Закона ECMA-335 Общий язык Инфраструктура

Type initialization alone shall not create a deadlock unless some code called from a type initializer (directly or indirectly) explicitly invokes blocking operations.

Следующий код приводит к тупиковой ситуации

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

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

Статический конструктор Конец запустит перед любому потоку, которому разрешен доступ к классу.

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

Другие вопросы по теме