О синглтонах и статических конструкторах

    Изначально автор хотел назвать эту статью следующим образом: «О синглтонах, статических конструкторах и инициализаторах статических полей, о флаге beforeFieldInit и о его влиянии на deadlock-и статических конструкторов при старте сервисов релизных билдов в .Net Framework 3.5», однако в связи с тем, что многострочные названия по неведомой автору причине так и не прижились в современном компьютерном сообществе, он (автор) решил сократить это название, чудовищным образом исказив его исходный смысл.

    -------------------------

    Любая реализация паттерна Синглтон в общем случае преследует две цели: во-первых, реализация должна быть потокобезопасной, чтобы предотвратить создание более одного экземпляра в многопоточном мире .Net; а во-вторых, эта реализация должна быть «отложенной» (lazy), чтобы не создавать экземпляр (потенциально) дорого объекта раньше времени или в тех случаях, когда он вообще может не понадобиться. Но поскольку основное внимание при прочтении любой статьи про реализацию Синглтона отводится многопоточности, то на «ленивость» зачастую не хватает ни времени не желания.



    Давайте рассмотрим одну из наиболее простых и популярных реализаций паттерна Синглтон (*), основанную на инициализаторе статического поля:

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

      // Explicit static constructor to tell C# compiler
      // not to mark type as beforefieldinit
      static Singleton()
      {}

      private Singleton()
      {}

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

    * This source code was highlighted with Source Code Highlighter.


    Потокобезопасность этой реализации основывается на том, что статический конструктор (или, другими словами, инициализатор типа) в одном домене гарантировано вызывается не более одного раза. А раз так, то со стороны разработчика не нужно делать никакие финты ушами ради того, чтобы сделать потокобезопасность еще более безопасной. Большинство разработчиков (и до недавнего времени и я в том числе) на этом успокаиваются, поскольку основную проблему, которая может произойти с любым синглтоном, мы уже решили, так что на комментарий пустого статического конструктора мало кто обращает внимание. А поскольку этот комментарий вообще нифига не понятен, то пустой статический конструктор просто не доходит до многих реализаций в реальных приложениях.

    Статический конструктор и инициализаторы полей



    Статический конструктор – это штука, предназначенная для инициализации типа, которая должна быть вызвана перед доступом к любому статическому или не статическому члену, а также перед созданием экземпляра класса. Однако если класс в языке C# не содержит явного объявления статического конструктора, то компилятор помечает его атрибутом beforeFieldInit, что говорит среде времени выполнения о том, что тип можно инициализировать отложенным (“relaxed”) образом. Однако, как показывает практика, в .Net Framework до 4-й версии, это поведение можно назвать каким угодно, но не «отложенным».

    Итак, давайте рассмотрим следующий код:

    class Singleton
    {
      //static Singleton()
      //{
      //  Console.WriteLine(".cctor");
      //}

      public static string S = Echo("Field initializer");
      
      public static string Echo(string s)
      {
        Console.WriteLine(s);
        return s;
      }
    }

    class Program
    {

      static void Main(string[] args)
      {
        Console.WriteLine("Starting Main...");
        if (args.Length == 1)
        {
          Console.WriteLine(Singleton.S);
        }
        Console.ReadLine();
      }
    }

    * This source code was highlighted with Source Code Highlighter.


    Поскольку в данном случае явный статический конструктор класса Singleton отсутствует, то компилятор к этому типу добавляет атрибут beforeFieldInit. Согласно спецификации, в этом инициализация статического поля произойдет до первого обращения к этому полю, причем может она может произойти задолго до этого обращения. На практике при использовании .Net Framework 3.5 и ниже, это приводит к тому, что инициализация статического поля произойдет до вызова метода Main, даже если условие args.Legnth == 1 не будет выполнено. Все это приводит к тому, что при запуске указанного выше кода мы получим следующее:

    Field initializer

    Starting Main...


    Как видно, что статическое поле будет проинициализировано, хотя сам тип в приложении не используется. Практика показывает, что в большинстве случаев при отсутствии явного конструктора, JIT-компилятор вызывает инициализатор статических переменных непосредственно перед вызовом метода, в котором используется эта переменная. Если раскомментировать статический конструктор класса Singleton, то поведение будет именно таким, которое ожидает большинство разработчиков – инициализатор поля вызван не будет и при запуске приложения на экране будет только одна строка: Starting Main…”.

    ПРИМЕЧАНИЕ

    Разработчик не может и не должен завязываться на время вызова статического конструктора. Если следовать «букве закона», то вполне возможна ситуация, когда в приведенном выше примере (без явного конструктора типа), переменная Singleton.S не будет проинициализирована при создании экземпляра класса Singleton и при вызове статического метода, который не использует поле S, но будет проинициализирована при вызове статической функции, использующей поле S. И хотя именно такое поведение исходно заложено в определение флага beforeFieldInit, в спецификации языка C# специально говорится о том, что точное время вызова определяется реализацией. Так, например, при запуске приведенного выше исходного фрагмента (без явного статического конструктора) под .Net Framework 4, мы получим более ожидаемое поведение: поле S проинициализировано не будет! Более подробно об этом можно почитать в дополнительных ссылках, приведенных в конце статьи.


    Статические конструкторы и взаимоблокировка



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

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

    class Program
    {
      static Program()
      {
        var thread = new Thread(o => { });
        thread.Start();
        thread.Join();
      }

      static void Main()
      {
        // Этот метод никогда не начнет выполняться,
        // поскольку дедлок произойдет в статическом
        // конструкторе класса Program
      }
    }

    * This source code was highlighted with Source Code Highlighter.


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

    Бага в реальном приложении



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

    Итак, вот симптомы реальной проблемы, с которой я столкнулся. У нас есть сервис, который прекрасно работает в консольном режиме, а также не менее прекрасно работает в виде сервиса, если собрать его в Debug-е. Однако если собрать его в релизе, то он запускается через раз: один раз запускается успешно, а во второй раз запуск падает по тайм-ауту (по умолчанию SCM прибивает процесс, если сервис не запустился за 30 секунд).

    В результате отладки было найдено следующее. (1) У нас есть класс сервиса, в конструкторе которого происходит создание счетчиков производительности; (2) класс сервиса реализован в виде синглтона с помощью инициализации статического поля без явного статического конструктора, и (3) этот синглтон использовался напрямую в методе Main для запуска сервиса в консольном режиме:

    // Класс сервиса
    partial class Service : ServiceBase
    {
      // "Кривоватая" реализаци Синглтона. Нет статического конструктора
      public static readonly Service instance = new Service();
      public static Service Instance { get { return instance; } }

      public Service()
      {
        InitializeComponent();

        // В конструкторе инициализирутся счетчики производительности
        var counters = new CounterCreationDataCollection();
          
        if (PerformanceCounterCategory.Exists(category))
          PerformanceCounterCategory.Delete(category);

        PerformanceCounterCategory.Create(category, description,
          PerformanceCounterCategoryType.SingleInstance, counters);
      }

      // Метод запуска сервиса
      public void Start()
      {}

      const string category = "Category";
      const string description = "Category description";
    }
    // А тем временем в классе Program
    static void Main(string[] args)
    {
      if (args[0] == "--console")
        Service.Instance.Start();
      else
        ServiceBase.Run(new Service());

    }

    * This source code was highlighted with Source Code Highlighter.


    Поскольку класс Server не содержит явного статического конструктора и компилятор C# добавляет флаг beforeFieldInit, то вызов конструктора класса Service происходит до вызова метода Main. При этом для создания категории счетчиков производительности используется именованный мьютекс, что в определенных условиях приводит к дедлоку приложения: во время первого запуска указанной категории еще нет в системе, поэтому метод Exists возвращает false и метод Create завершается успешно; во время следующего запуска метод Exists возвращает true, метод Delete завершается успешно, но метод Create подвисает на веки. Понятное дело, что после того, как проблема была найдена, решение заняло ровно 13 секунд: добавить статический конструктор в класс Service.

    Заключение



    Пример с багом в реальном приложении говорит о том, что статьи о подводных камнях языка C# и о правильном применении известных паттернов и идиом не является бредом и выдумкой теоретиков (**), многие подобные статьи основываются на шишках, набитых в реальном мире. Сегодня вы могли столкнуться с проблемами кривой реализации синглтона, завтра – с непонятным поведением изменяемых значимых типов, послезавтра вы прибиваете поток с помощью Thread.Abort и получаете рассогласованное состояние системы (***). Все эти проблемы весьма реальны и понимание принципов, заложенных в их основу может сэкономить денек другой при поиске какого-нибудь особенно злого бага.

    Дополнительные ссылки





    -------------------------

    (*) Двумя другими весьма популярными реализациями паттерна Синглтон являются: (1) блокировка с двойной проверкой (double checked locking), а также (2) с помощью типа Lazy<T>, который появился в .Net Framework 4.

    (**) Один мой коллега свято верит в то, что книги и статьи читать не имеет смысла, поскольку их пишут те, кто ничего не смыслит в настоящей разработке ПО. Поэтому он считает, что существует только одна «настоящая» реализация синглтона, и что нужно добавлять пустой финализатор всем классам, реализующим интерфейс IDisposable.

    (***) Если интересно, какие такие проблемы таятся в изменяемых значимых типах, то вполне подойдет предыдущая заметка «О вреде изменяемых значимых типов», ну а если интересно, что же такого плохого в вызове Thread.Abort, то тут есть даже две заметки: «О вреде вызова Thread.Abort», а также перевод интересной статьи Криса Селлза «Изучение ThreadAbortExcpetion с помощью Rotor».
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 35

      +10
      public class MyClass
      {
        private static readonly Lazy<MyClass> instance = new Lazy<MyClass>(() => new MyClass());
        private MyClass(){}
        public static MyClass Instance
        {
          get { return instance.Value; }
        }
      }

      Самый понятный подход, в отличии от надежды на какой-то спецэффект IL-генерации.
        +3
        Совершенно согласен, но не каждого разработчика радует наличие 4-го фреймворка. Многие все еще сидят на предыдущих версиях.
          0
          Ну Lazy не настолько сложен, его можно и руками набросать, если проект не будет использовать .NET F 4
            0
            Самостоятельная реализация Lazy этот подход делает уже не столь привлекательным, ИМХО. Хотя, конечно, можно просто взять исходники фреймворка и перетянуть его в свою библиотеку.
          –3
          Классная идея.

          Я обычно что-то такое использовал:
          public class SomeClass
          {
            private SomeClass _instance;
            private SomeClass() {}
          
            private SomeClass Instance
            {
              if (_instance == null)
                _instance = new SomeClass();
              return _instance;
            }
          }
          
            +3
            ну, этот код не совсем потокобезопасный.
            Если два потока одновременно попытаются в первый раз достучаться к инстансу, у нас получится два объекта.

            З.Ы. Пару лет назад я отладивал багу, которая была из-за непотокобезопасного синглтона. Так что я бы дважды подумал, прежде чем использовать подобный код.
              0
              Согласен полностью.

              Теперь у меня просто один singleton, в котором все остальные сидят. И я контролирую его создание.
              Это для игры такое дело, есть singleton GameEngine, который содержит в себе уже инстансы на различные сервисы: TextureManager, SoundManager, UiSystem, TriggerService и т.д.
              +1
              клево, конечно, но как вы ЭТО могли использовать?! Я так думаю вы static и public забыли. Это минимально.
              А еще (даже если будет static public SomeClass Instance) у вас это не потоко безопасно. Вам же написали вконце: надо делать double lock в такой реализации…
                0
                Я не понял, где и что я забыл. У меня есть класс Service, с открытым статическим свойством типа Service. Обращение к этому свойство абсолютно потокобезопасно, поскольку CLR гарантирует только один вызов статического конструктора.

                > Вам же написаи вконце: надо делать double lock в такой реализации…
                В какой реализации? Еще раз напомню, с потокобезопаснотью класса Service тотальное ОК.
                  +1
                  Сергей, не нервничайте. jonie комментировал не ваш код ;-)
                    0
                    Упс! Сори. Не всегда тут понятно, кто на что отвечает:(
                  0
                  Да забыл :)
                  Даешь Resharper встроенный на хабре.
                  0
                  Стандартный пример синглтона вроде бы даже с msdn/
                  public sealed class Singletone
                  {
                  private static Singletone instance;
                  private object sync = new Object();

                  public static Singletone GetInstance()
                  {
                  lock(sync)
                  {
                  if(instance == null)
                  instance = new Singletote()
                  }
                  return instance;
                  }

                  }
                    0
                    Если честно, то msdn — это далеко не самый лучший источник информации по правильному применению идиом, паттернов, да и качество кода, там зачастую на букву г.

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

                    Именно поэтому более предпочтительным вариантом является следующий:
                    public static Singleton GetInstance()
                    {
                    // Если это условие не выполнится, то никакие блокировки не нужны, поскольку
                    // экземпляр синглтона уже создан
                    if ( instance == null )
                    {
                    // Но поскольку никаких блокировок сделано не было, есть вероятность,
                    // что произошло переключение контекста и другой поток успел создать
                    // экземпляр синглтона, поэтому захватываем блокировку и проверяем
                    // наличие экземпляра еще раз
                    lock(sync)
                    {
                    if (instance == null)
                    {
                    // Да, экземпляр таки не создался, давайте-ка создадим его
                    instance = new Singleton();
                    }
                    }
                    }
                    return instance;
                    }


                    З.Ы. За подробностями прошу к Скиту: Implementing the Singleton Pattern in C#.
                      0
                      Не… в MSDN предпочитают перестраховаться и сделать так:

                      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;
                      }
                      }
                      }
                  +1
                  Познавательно

                  Но я так и не понял почему во втором случае «метод Create подвисает на веки». Можно пояснить?
                    0
                    Воспроизводится это дело с полпинка, а вот понять, где именно там лочится, пока так и не понял.
                    –2
                    public static Service instance;
                    public static Service Instance {

                    get {
                    if(instance==null)
                    instance = new Service();

                    return instance;

                    }

                    }

                    так, не?
                      0
                      уппс, меня опередили, пока писал. :(
                        +1
                        Ну и ответ тот же. Код не потокобезопасен;)
                          0
                          В любом случае, спасибо за статью, особо Singletone я пока не пользовался, но в будущем где-то придется.
                            +1
                            К этому паттерну вообще, нужно относиться с осторожностью. Именно в том проекте, о котором вскользь идеть речь в статье, можно было бы избавиться как минимум от половины синглтонов.

                            В большинстве случаев, это скорее антипаттерн, нежели паттерн. Поскольку синглтон — есть глобальная переменная, а они (эти глобальные переменные) являются не самым лучшим подходом проектирования. С ними аккуратнее нужно быть:)
                      0
                      Ах Рихтер, редиска, забыл про необходимость статического конструктора упомянуть! (в его книге написано, что простой статик филд эквивалентен способу с двойной блокировкой).
                        +1
                        Еще раз напомню, что статик филд с точки зрения потокобезопасности и так эквивалентен двойной блокировке. Проблемы возможны не с многопоточностью, а со временем создания экземпляра: он может быть создан не совсем тогда, когда вы этого ожидаете.

                        З.Ы. Рихтер в поряде, у него есть неплохой раздел по поводу флага beforefieldinit (хотя семантика названия описана у него не совсем точно, и, кажись, не сказано о том, что это злой implementation detail).
                        0
                        А для чего нужен ленивый синглтон? Обычно он описывается небольшой структурой, и как бы непонятно зачем экономить? Если ж подобных этому классу — их тыщи и миллионы, то тогда другой вопрос — зачем столько синглтонов? Непонятно…
                          0
                          У ленивости есть несколько плюсов:
                          1. Ресурс может вообще не понадобиться, а мы его создали. Синглтон может быть и тяжеловесной хренью.
                          2. Недетерменированное создание ресурса чревато. Если конструктор синглтона вдруг может падать, то это падение будет происходить в совершенно непонятных местах. В случае же ленивого обращения, нам хотя бы будет понятна причина и место возникновение ошибки.
                            0
                            Вот я про то же: вроде бы может быть тяжеловесной хренью, но вот так вот чтобы придумать сразу на гора десяток различных примеров… — у меня и одного не получается. Все, что приходит в голову, является примером объектов несколько другого рода, и тогда Singleton — это вроде как неуместное применение паттерна. Расскажите о примере боевого использования тяжелого синглтона, чтоб отмести все подозрения?
                              0
                              Я привел пример, когда отсутствие ленивой инициализации приводило к дедлоку приложения. Ну и меня больше беспокоит не пустая трата ресурсов, что плохо, но не смертельно, а недерменированность исключений, когда аппликуха начинает падать и ты понятия не имеешь, в каком из 20 синглтонов произошла ошибка.
                            0
                            Например, настройки приложения могут быть довольно большие, а конкретный сеанс к ним может и не обращаться.
                              0
                              null, undefined, none, void — то что приходит на ум в виде примеров. Настройки — уже не настоль хороший пример, поскольку настройки могут прочитаться, или нет: из файла или иным путем. А синглтон должен существовать более-менее неизменным на протяжении не только всей жизни процесса. А если он ленивый? Код, описывающий его инициализацию может занять больше места, чем то место, на которое ссылается указатель. Если конфигурация должна быть одна на весь процесс — пожайлуста. Должен ли это быть синглтон — совершенно не обязательно. Ведь из чего исходят, задумывая конфигурацию синглтоном: нужна гарантия единственности? А как насчет других гарантий, что дают юнит тесты, и с которыми синглтоны не очень дружны по своей природе? Конечно дело вкуса, но вопрос остается открытым — наверное есть действительно какие-то примеры использования синглтона, если этому посвящено столько статей на хабре — однозначно должно быть парочка значимых, там, где критерий единственности был бы настолько потрясающим решением задачи, что прям — «вау». Я просто не вижу — откройте мне веки.
                            +2
                            Как вывод, синглтон на статических классах вполне себе работоспособен, если приучить себя не злоупотреблять инициализаторами в объявлениях, а всегда выносить их в статический конструктор.

                            Да и для обычных, не статических классов этот совет, кажется, тоже может быть актуален.
                              +1
                              Какой-то у вас синглтон недоделанный…

                              if (args[0] == "--console")
                              Service.Instance.Start(); // True singleton
                              else
                              ServiceBase.Run(new Service()); // Совсем уже и не синглтон
                                0
                                Сори, код немного не правильный. На самом деле должно быть:
                                if (args[0] == "--console")
                                Server.Instance.Start();
                                else
                                ServiceBase.Run(new Service());

                                А внутри обработчика OnStart сервиса снова вызывается метод Server.Instance.Start();
                                0
                                Что-то я недопонял как указание явного статичекого конструктора решает проблему в целом. Получается что изменился момент инициализации статического поля, но вы же сами пишите:
                                Поскольку статический конструктор указанного типа должен вызываться не более одного раза в домене приложения, то CLR вызывает его внутри некоторой блокировки. Тогда, если поток, исполняющий статический конструктор будет ожидать завершения другого потока, который в свою очередь попытается захватить ту же самую внутреннюю блокировку CLR, мы получим классический дедлок.
                                Т.е. дедлок по прежнему возможен. Имхо стоит все таки разобраться почему именно виснет Create.
                                  0
                                  Да, дедлок возможен, но в данном конкретном случае он происходил именно при вызове этого дела до функции Main и не происходил, если он вызывался при первом обращении внутри функции Main.

                                  Инициализация счетчиков производительности в статическом конструкторе все ще может привести к дедлоку, поэтому наиболее оптимальным решением является переход на double ckecked lock (или на Lazy).

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое