Menu
Coddy logo textTech

Потокобезопасный Singleton

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 52 из 70.

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

Базовый Singleton использует приватный конструктор для предотвращения создания экземпляров извне и статическое свойство для предоставления доступа:

public class Logger
{
    private static Logger _instance;
    private static readonly object _lock = new object();
    
    private Logger() { }  // Приватный конструктор
    
    public static Logger Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                    _instance = new Logger();
                return _instance;
            }
        }
    }
    
    public void Log(string message) => Console.WriteLine(message);
}

Оператор lock обеспечивает потокобезопасность — если несколько потоков попытаются получить доступ к Instance одновременно, только один сможет войти в критическую секцию в конкретный момент времени. Без этого два потока могли бы одновременно увидеть, что _instance равен null, и создать отдельные экземпляры.

C# предлагает более простой потокобезопасный подход с использованием статической инициализации:

public class Logger
{
    private static readonly Logger _instance = new Logger();
    
    private Logger() { }
    
    public static Logger Instance => _instance;
    
    public void Log(string message) => Console.WriteLine(message);
}

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

Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");
challenge icon

Задание

Легко

Давайте создадим потокобезопасный менеджер конфигурации, используя паттерн Одиночка (Singleton). В реальных приложениях настройки конфигурации должны быть доступны во всей программе, при этом необходимо гарантировать существование только одного экземпляра — это идеальный случай использования Singleton.

Вы организуете свой код в двух файлах:

  • ConfigManager.cs: Создайте класс ConfigManager в пространстве имен Configuration, который реализует паттерн Singleton. Ваш менеджер конфигурации должен:
    • Иметь закрытое статическое поле для хранения единственного экземпляра
    • Использовать закрытый конструктор для предотвращения создания экземпляров извне
    • Предоставлять статическое свойство Instance, которое возвращает единственный экземпляр (используйте более простой подход со статической инициализацией для обеспечения потокобезопасности)
    • Включать закрытое строковое поле _appName, инициализированное значением "MyApp"
    • Иметь метод SetAppName(string name) для обновления названия приложения
    • Иметь метод GetAppName(), который возвращает текущее название приложения
  • Program.cs: В основном файле продемонстрируйте правильную работу Singleton, обратившись к экземпляру несколько раз и показав, что изменения сохраняются во всех ссылках.

Вы получите один входной параметр:

  • Новое название приложения для установки (например, ProductionServer)

В вашей программе:

  1. Получите экземпляр ConfigManager и выведите название приложения по умолчанию
  2. Получите экземпляр снова (в другую переменную) и используйте его для установки нового названия приложения из входных данных
  3. Получите экземпляр в третий раз и выведите название приложения, чтобы доказать, что все ссылки используют одни и те же данные

Например, если на вход подается ProductionServer, вывод должен быть следующим:

MyApp
ProductionServer

Это демонстрирует основное преимущество Singleton — сколько бы раз вы ни обращались к Instance, вы всегда получаете один и тот же объект с общим состоянием. Подход со статической инициализацией гарантирует потокобезопасность, так как CLR гарантирует, что статическое поле инициализируется только один раз, даже если несколько потоков обращаются к нему одновременно.

Шпаргалка

Паттерн Singleton (Одиночка) гарантирует, что у класса есть только один экземпляр во всем приложении, и предоставляет к нему глобальную точку доступа.

Базовая реализация Singleton с явной блокировкой для обеспечения потокобезопасности:

public class Logger
{
    private static Logger _instance;
    private static readonly object _lock = new object();
    
    private Logger() { }  // Приватный конструктор
    
    public static Logger Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                    _instance = new Logger();
                return _instance;
            }
        }
    }
    
    public void Log(string message) => Console.WriteLine(message);
}

Оператор lock обеспечивает потокобезопасность, позволяя только одному потоку входить в критическую секцию одновременно.

Более простой потокобезопасный Singleton с использованием статической инициализации:

public class Logger
{
    private static readonly Logger _instance = new Logger();
    
    private Logger() { }
    
    public static Logger Instance => _instance;
    
    public void Log(string message) => Console.WriteLine(message);
}

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

Использование — всегда возвращается один и тот же экземпляр:

Logger.Instance.Log("First call");
Logger.Instance.Log("Same instance");

Попробуйте сами

using System;
using Configuration;

class Program
{
    public static void Main(string[] args)
    {
        // Считать ввод — новое имя приложения
        string newAppName = Console.ReadLine();
        
        // TODO: Получить экземпляр ConfigManager и вывести имя приложения по умолчанию
        
        // TODO: Получить экземпляр снова (в другую переменную) и использовать его для установки нового имени приложения
        
        // TODO: Получить экземпляр в третий раз и вывести имя приложения, чтобы доказать, что все ссылки используют одни и те же данные
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование