Перейти к содержанию

Последнее обновление:2026-10-13

Паттерн «одиночка» в C++

0. Введение

Наверняка никому не избежать на собеседованиях темы паттернов проектирования. В этом разделе разбирается самый популярный на собеседованиях паттерн — «одиночка» (singleton): его разновидности, потокобезопасность и решения потокобезопасного синглтона как без использования стандарта C++11, так и на его основе. Внимательно прочитайте эту статью — и на будущих собеседованиях вам беспокоиться будет не о чем.

1. Широко известный синглтон

В обычных книгах, или как принято считать, паттерн «одиночка» выглядит так:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}

Это очень простая реализация: конструктор объявляется private или protected, чтобы внешние функции не могли создать экземпляр; внутри статический указатель класса хранит единственный экземпляр; создание экземпляра выполняется public-методом, который и возвращает единственный экземпляр класса.

Разумеется, этот код годится только для однопоточного использования; при многопоточности он небезопасен. Представьте, что два потока впервые одновременно вызывают метод instance и оба обнаруживают, что p равно nullptr: тогда оба потока одновременно создадут экземпляр и запишут его в p, что нарушает принцип синглтона.

2. «Ленивая» и «жадная» инициализация

Синглтон реализуется двумя способами:

  • Ленивая инициализация
  • Экземпляр создаётся при первом использовании; приведённый выше вариант — ленивая реализация.
  • Жадная инициализация
  • Экземпляр создаётся в момент определения класса-синглтона.

Приведём и жадную реализацию:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

Разумеется, она потокобезопасна. Обычно же как небезопасную для потоков описывают ленивую разновидность; ниже мы разберём оптимизации кода ленивого синглтона для потокобезопасности.

3. Блокировка для многопоточности

В C++ есть класс для блокировки, принцип реализации которого основан на RAII и который не требует ручного управления unlock — это lock_guard; воспользуемся им для блокировки.

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

При такой записи не может случиться описанная выше ситуация, когда оба потока выполняются внутри ветки p == nullptr. Пока поток A выполняет p = new Singleton(), если поток B вызовет instance(), он гарантированно заблокируется на захвате мьютекса и будет ждать, пока поток A не завершит работу и не освободит его. Следовательно, это потокобезопасно.

Но такая запись крайне неэффективна, поскольку при каждом вызове instance() выполняется захват и освобождение мьютекса, а нужен этот шаг только для первого new Singleton(). Как только p создан, сколько бы потоков ни обращалось одновременно, достаточно проверки if (p == nullptr) (это лишь операция чтения, блокировка не нужна); проблемы потокобезопасности нет, а после добавления блокировки возникает как раз проблема производительности.

Отсюда появляется DCL.

4. Паттерн двойной проверки блокировки (DCLP)

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

singleton* singleton::instance() {
    if(p == nullptr) {  // 第一次检查
        Lock lock;
        if(p == nullptr){ // 第二次检查
            p = new singleton;
        }
    }
    return p;
}

На основе сказанного можно написать двойную проверку с блокировкой + автоматическое освобождение

class singleton {
private:
    singleton() {}

    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();

    // 实现一个内嵌垃圾回收类
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if(singleton::p)
                delete singleton::p;
        }
    };
    static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};

singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
std::mutex singleton::lock_;

singleton* singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
    }
    return p;
}

Суть DCLP в том, что большинство вызовов instance увидят p ненулевым и даже не попытаются его инициализировать. Поэтому DCLP перед попыткой захвата мьютекса проверяет, нулевой ли p. Только если проверка успешна (то есть p ещё не инициализирован), он захватывает мьютекс, а затем снова проверяет, остался ли p нулевым (отсюда и название — двойная проверка с блокировкой). Вторая проверка необходима, потому что, как мы только что видели, вполне вероятно, что другой поток случайно инициализирует p после первой проверки, но до успешного захвата мьютекса.

Код выше выглядит прекрасно, однако лишь по прошествии немалого времени в нём обнаружили изъян. Причина: переупорядочивание чтений и записей памяти (проблема компилятора).

Рассмотрим ещё раз строку, инициализирующую p:

p = new singleton;

Этот оператор влечёт три события:

  1. Выделить память, способную хранить объект singleton;
  2. Сконструировать объект singleton в выделенной памяти;
  3. Заставить p указывать на эту выделенную память.

Можно подумать, что эти три шага выполняются по порядку, но на деле гарантировано лишь то, что шаг 1 выполняется первым; шаги 2 и 3 — не обязательно. В этом и кроется проблема.

  • Поток A вызывает instance, выполняет первую проверку p, захватывает мьютекс, выполняет шаги 1 и 3, после чего приостанавливается. В этот момент p уже не нулевой, но в памяти, на которую указывает p, объект Singleton ещё не сконструирован.
  • Поток B вызывает instance, видит p ненулевым и возвращает его вызывающей стороне. Та разыменовывает указатель, чтобы получить singleton — и опа, объект, который ещё не был сконструирован. Вот и баг.

DCLP работает корректно, только если шаги один и два выполняются перед шагом три, но выразить такое ограничение в C или C++ невозможно. Это как кинжал в сердце DCLP: нам нужно наложить ограничения на относительный порядок инструкций, но наш язык не даёт способа выразить такие ограничения.

5. Инструкция memory barrier

Проблема DCLP была решена в C++11.

Поскольку новый C++11 определил новую модель памяти, гарантируется, что при выполнении указанных трёх шагов переключения потоков не произойдёт — процесс инициализации фактически является «атомарной» операцией, и DCL снова можно использовать корректно. Впрочем, в C++11 существует и более лаконичный способ написать многопоточный синглтон — о нём расскажем позже.

До C++11 решением была инструкция barrier. Чтобы всё выполнялось корректно, между шагами 2 и 3 нужно поставить memory barrier, вынуждая CPU выполнять шаги строго в порядке 1, 2, 3.

Первая реализация:

На основе operator new + placement new, код пишется последовательно в порядке выполнения 1, 2, 3.

// method 1 operator new + placement new
singleton *instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr) {
            singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
            new(tmp)singleton();
            p = tmp;
        }
    }
    return p;
}

Вторая реализация:

На основе непосредственной вставки ASM-инструкции mfence; макрос barrier в unix тоже реализован через эту инструкцию.

#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        barrier();
        if (p == nullptr) {
            p = new singleton();
        }
    }
    return p;
}

Обычно вызывается одна инструкция, предоставляемая CPU; её роль — не позволять CPU переставлять инструкции, стоящие до неё, после неё; такую инструкцию обычно и называют barrier. В коде выше asm означает, что это ассемблерная инструкция; volatile необязателен — если он использован, то это объявление компилятору, что оптимизировать эту ассемблерную инструкцию не разрешено. lwsync — инструкция barrier, предоставляемая POWERPC.

6. Статические локальные переменные

Скотт Мейерс в «Effective C++» предложил лаконичный способ записи синглтона

singleton &singleton::instance() {
    static singleton p;
    return p;
}
  • В однопоточном режиме — корректно.
  • В многопоточном режиме в C++11 и более поздних версиях (например, C++14) — корректно.
  • В многопоточном режиме до C++11 — не обязательно корректно.

Причина в том, что в стандартах до C++11 модель памяти для локальных static-переменных не была определена, поэтому он не был потокобезопасным. В C++11 же он потокобезопасен, потому что новый стандарт C++ требует: пока один поток инициализирует переменную, остальные потоки обязаны дождаться завершения инициализации, прежде чем получить к ней доступ.

Использованные выше порядки памяти:

  • memory_order_relaxed: ослабленный порядок памяти; гарантирует лишь атомарность операций над атомарным объектом
  • memory_order_acquire: операция получения; при чтении атомарного объекта никакие последующие чтения или записи текущего потока не могут быть переупорядочены до этой операции, и все записи памяти, сделанные другими потоками до освобождения того же атомарного объекта, становятся видимыми текущему потоку
  • memory_order_release: операция освобождения; при записи в атомарный объект никакие предшествующие чтения или записи текущего потока не могут быть переупорядочены после этой операции, и все записи памяти текущего потока становятся видимыми другим потокам, которые получают тот же атомарный объект

7. Atomic

В версиях до C++11, помимо реализации потокобезопасного Singleton с помощью блокировок, можно использовать встроенные в компиляторы atomic operation.

Обнаружив проблему переупорядочивания, java и c# добавили ключевое слово volatile: при объявлении переменной p нужно указать квалификатор volatile, и, увидев его, компилятор понимает, что здесь переупорядочивать нельзя (сначала выделить память, затем выполнить конструктор и только после всего этого выполнить присваивание).

Стандарт c++ же долго не исправляли, поэтому VC++ добавил это ключевое слово в версии 2005, но оно не является кроссплатформенным (поддерживаются только платформы Microsoft).

А в c++ 11, чтобы устранить эти дыры в корне, была введена подходящая для многопоточности модель памяти. Наконец-то появился механизм, помогающий реализовать переносимое решение.

mutex singleton::lock_;
atomic<singleton *> singleton::p;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
    singleton *tmp = p.load(memory_order_relaxed);
    atomic_thread_fence(memory_order_acquire);
    if (tmp == nullptr) {
        lock_guard<mutex> guard(lock_);
        tmp = p.load(memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new singleton();
            atomic_thread_fence(memory_order_release);
            p.store(tmp, memory_order_relaxed);
        }
    }
    return p;
}

Примечательно, что в коде выше используются два довольно ключевых термина — получение и освобождение:

  • Получение — это операция чтения памяти; никакие последующие чтения или записи текущего потока не могут быть переупорядочены до неё.
  • Освобождение — это операция записи в память; никакие предшествующие чтения или записи текущего потока не могут быть переупорядочены после неё.

acquire и release обычно появляются в паре, и цель этого — гарантировать, что если освобождение (release) одного и того же атомарного объекта происходит до получения (acquire), то все изменения памяти, случившиеся до release, будут полностью видны чтениям памяти после acquire.

8. pthread_once

На unix-платформах, помимо atomic operation, если C++11 недоступен, Singleton можно реализовать и через pthread_once.

Прототип таков:

int pthread_once(pthread_once_t once_control, void (init_routine) (void));

Реализация:

class singleton {
private:
    singleton(); //私有构造函数,不允许使用者自己生成对象
    singleton(const singleton &other);

    //要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
    static void init() {
        p = new singleton();
    }

    static pthread_once_t ponce_;
    static singleton *p; //静态成员变量 
public:
    singleton *instance() {
        // init函数只会执行一次
        pthread_once(&ponce_, &singleton::init);
        return p;
    }
};

9. Итоги

В этой статье разобраны несколько вариантов синглтона, в частности потокобезопасные синглтоны, а также несколько потокобезопасных реализаций без C++11: memory barrier, статические локальные переменные, способ с pthread_once, реализация на atomic из C++11 и т. д.

Напоследок отметим: что касается деструктора описанных выше классов-синглтонов, см. реализацию «двойная проверка с блокировкой + автоматическое освобождение» — в классе обязательно нужно объявить статическую локальную переменную. Статическую локальную переменную можно понимать как глобальную: при завершении программы её деструктор вызывается автоматически. Вот почему внутри класса объявляется и определяется вложенный класс, а не пишется напрямую деструктор синглтона.

10. Ссылки

https://thorns.cn/2019/03/09/DCLP.html

https://www.cnblogs.com/liyuan989/p/4264889.html

https://segmentfault.com/a/1190000015950693

Переиспользуемый шаблон потокобезопасного синглтона

Отвечая на запрос из issue #303: следуя идее boost::container::singleton_default, шаблонный базовый класс на CRTP может превратить любой класс в потокобезопасный синглтон одной строкой (полный код см. в singleton_default.hpp, включая тесты конкурентного доступа):

template <class T>
class SingletonDefault {
public:
    static T& instance() {
        static T inst;   // C++11 起局部静态变量初始化线程安全
        return inst;
    }
    SingletonDefault(const SingletonDefault&) = delete;
    SingletonDefault& operator=(const SingletonDefault&) = delete;
protected:
    SingletonDefault() = default;
};

// 用法:继承即可
class MyConfig : public SingletonDefault<MyConfig> {
    friend class SingletonDefault<MyConfig>;
private:
    MyConfig() = default;   // 构造私有,交由基类创建
public:
    int value = 42;
};

// MyConfig::instance().value

Комментарии