← Назад к вопросам

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

2.0 Middle🔥 251 комментариев
#Многопоточность и синхронизация#ООП и проектирование#Язык C++

Условие

Реализуйте потокобезопасный паттерн Singleton на C++11 или выше.

Класс должен гарантировать:

  • Ровно один экземпляр класса создаётся за всё время работы программы
  • Потокобезопасность при первом обращении из нескольких потоков
  • Ленивую инициализацию (объект создаётся при первом обращении)

Требования

  • Используйте возможности C++11 и выше
  • Запретите копирование и перемещение объекта
  • Метод getInstance() должен возвращать ссылку на единственный экземпляр

Пример использования

class Logger : public Singleton<Logger> {
public:
    void log(const std::string& message);
};

// Использование
Logger::getInstance().log("Application started");

Бонус

Объясните, почему решение через статическую локальную переменную (Scott Meyers Singleton) является потокобезопасным начиная с C++11.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Потокобезопасный Singleton на C++11

Лучший подход: Magic Statics (Scott Meyers)

Начиная с C++11 стандарт гарантирует потокобезопасную инициализацию локальных статических переменных. Это самое элегантное и надёжное решение:

template <typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }
    
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;
    
protected:
    Singleton() = default;
    ~Singleton() = default;
};

Использование

class Logger : public Singleton<Logger> {
public:
    void log(const std::string& message) {
        std::cout << "[LOG] " << message << std::endl;
    }
    
protected:
    friend class Singleton<Logger>;
    Logger() { std::cout << "Logger initialized" << std::endl; }
};

// Использование
int main() {
    Logger::getInstance().log("Application started");
    Logger::getInstance().log("Working...");
    return 0;
}

Почему это потокобезопасно в C++11?

Стандарт C++11 5.2.3 (Variable scope) и 3.3.7 (Block scope) явно гарантируют:

  1. Guard переменная: Компилятор автоматически генерирует скрытую boolean-переменную для проверки инициализации
  2. Atomicity: Проверка и инициализация статической переменной атомарны на уровне языка
  3. Синхронизация потоков: ОС предоставляет механизм синхронизации (mutex на Linux/Windows) для защиты инициализации
  4. Гарантии: Если поток A начал инициализировать, остальные потоки ждут завершения

Детальное объяснение механизма

ДО C++98: Двойная проверка блокировки (Double Checked Locking) была нужна — сложная, подвержена ошибкам.

C++11 и позже: Компилятор генерирует эквивалент:

static T& getInstance() {
    static __guard flag = 0;  // скрытая переменная
    if (!flag) {
        __mutex.lock();
        if (!flag) {
            new (&instance) T();  // placement new
            flag = 1;
        }
        __mutex.unlock();
    }
    static T instance;
    return instance;
}

Гарантии стандарта:

  • Инициализация происходит ровно один раз
  • Если несколько потоков вызовут getInstance() одновременно, один выполнит инициализацию, остальные дождутся
  • Нет race conditions, нет deadlock'ов

Альтернативные подходы

1. Удаляемые конструкторы (как в моём коде) — запрещают копирование:

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

2. Private конструктор (старый стиль):

private:
    Singleton() {}

3. Protected конструктор с friend (для наследования) — подходит для шаблонов:

protected:
    friend class Singleton<T>;
    Singleton() = default;

Тестирование многопоточности

#include <thread>
#include <vector>

int main() {
    std::vector<std::thread> threads;
    std::set<Logger*> instances;
    
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&instances]() {
            Logger& logger = Logger::getInstance();
            instances.insert(&logger);
        });
    }
    
    for (auto& t : threads) t.join();
    
    assert(instances.size() == 1);  // ровно один экземпляр!
    return 0;
}

Выводы

Magic Statics — оптимальное решение для C++11+ ✅ Встроенная потокобезопасность без явных мьютексов ✅ Ленивая инициализация (объект создаётся при первом обращении) ✅ Запрет копирования/перемещения через = delete ✅ Чистый, читаемый, стандартный код