Потокобезопасный Синглтон на C#

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

Синглтон – шаблон, гарантирующий, что в приложении будет единственный экземпляр некоторого класса, и предоставляющий глобальную точку доступа к этому экземпляру [wiki]. Где-то ещё может быть написанно, что этот шаблон эмулирует глобольные переменные в ООП-языках, таких как C# и Java.

Собственно из-за схожести с глобальными переменными возникает главная претензия к Синглтону – из-за него усложняется тестирование (Unit-тестами), и появляются взаимные зависимости между модулями. В самых терминальных случаях Синглтон начинает использоваться как средство двунаправленного обмена данными между модулями, и логика становится совсем уж запутанной.

С другой стороны – синглтоны иногда все-таки необходимы. Например - для контроля доступа к какому-нибудь ресурсу, кэширования, управления конфигурацией и т.д.

Давайте разберем варианты его реализации:

  1. Самое простое решение:
    public class Singleton
    {
        private static Singleton _instance;

        private Singleton() { }

        public static Singleton Instance => _instance ?? (_instance = new Singleton());
    }

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

  1. Добавим лок:
    public class Singleton
    {
        private static Singleton _instance;
        private static readonly object _locker;

        private Singleton() { }

        public static Singleton Instance
        {
            get
            {
                lock (_locker)
                {
                    return _instance ?? (_instance = new Singleton());
                }
            }
        }
    }

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

  1. Double-checking
    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_locker)
                {
                    if (_instance == null)
                        _instance = new Singleton();
                }
            }

            return _instance;
        }
    }

Это самое верное (с алгоритмической точки зрения) решение, которое само по себе является паттерном под названием Double-checked locking. Обратите внимание, что в статье указывается на необходимость ключевого слова volatile, что связанно с особенностями Memory-Model, однако это отдельная история.

У нас получился довольно развесистый код, в котором есть много инфраструктурной логики, и совсем нет бизнес-логики. Если кому-то захотелось вынести инфраструктурный код в отдельный класс – это правильное желание, однако все уже сделано до нас. Начиная с 4ой версии фреймворка появился класс Lazy который делает тоже самое.

  1. DI-контейнер

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

В 2017 году (и последние лет 5 тоже), давно пора научиться пользоваться DI-контейнерами, в каждом из которых есть опция регистрации зависимости .AsSingleInstance() или подобная, что делает вашу зависимость синглтоном, но при этом позволяет ее легко и просто подменять моком в юнит-тестах.

Итоги

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

Однако, используете ли вы Lazy, готовый DI-контейнер или любой другой код, написанный не вами, всегда полезно понимать что стоит за интерфейсом скачанной библиотеки, для того чтобы уметь управлять вашим приложением, ведь все проблемы, которые есть в чужом коде, сразу же становится вашими проблемами, как только вы начинаете его использовать у себя.


comments powered by HyperComments
Яндекс.Метрика