Ir para o conteúdo

Última atualização:2026-10-13

O padrão singleton em C++

0. Introdução

Com certeza ninguém escapa do assunto de padrões de projeto em entrevistas. Esta seção expõe o padrão de projeto mais usado em entrevistas (o padrão singleton), abordando sua classificação, a segurança de threads e as soluções de singleton thread-safe tanto sem o padrão C++11 quanto com base nele. Leia este artigo com atenção e não precisará se preocupar em entrevistas futuras.

1. O singleton amplamente conhecido

Nos livros comuns, ou conforme amplamente conhecido, o padrão singleton é o seguinte:

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;
}

Esta é uma implementação muito simples: o construtor é declarado private ou protected para impedir que funções externas instanciem a classe; internamente, um ponteiro estático da classe guarda a única instância; a instanciação é realizada por um método public, que retorna a única instância da classe.

É claro que esse código só serve para uso single-threaded; com múltiplas threads, não é seguro. Considere duas threads chamando instance pela primeira vez ao mesmo tempo e ambas detectando p como nullptr: as duas threads construiriam uma instância para p simultaneamente, violando o princípio do singleton.

2. Inicialização preguiçosa e ansiosa

O singleton tem duas formas de implementação:

  • Inicialização preguiçosa (lazy)
  • A instanciação só acontece quando a instância da classe é usada pela primeira vez; o código acima é a implementação preguiçosa.
  • Inicialização ansiosa (eager)
  • A instanciação acontece no momento em que a classe singleton é definida.

Aqui também mostramos a implementação ansiosa:

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

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

Esta, é claro, é thread-safe. A que normalmente descrevemos como não thread-safe é a variante preguiçosa; a seguir explicamos as otimizações de código thread-safe para o padrão preguiçoso.

3. Bloqueio para múltiplas threads

Em C++ há uma classe para bloqueio cujo princípio de implementação usa RAII, sem necessidade de gerenciar o unlock manualmente — lock_guard; usamos aqui para bloquear.

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;
}

Com esta versão, não ocorre a situação acima em que as duas threads executam dentro do ramo p == nullptr. Enquanto a thread A executa p = new Singleton(), se a thread B chamar instance(), ela com certeza bloqueará na aquisição do lock e esperará até que a thread A termine e o libere. Portanto, é thread-safe.

Mas essa versão tem desempenho muito baixo, porque toda chamada a instance() adquire e libera o lock, e essa etapa só é necessária para o primeiro new Singleton(). Uma vez que p foi criado, não importa quantas threads acessem simultaneamente, o teste com if (p == nullptr) é suficiente (é apenas uma operação de leitura, sem necessidade de lock); não há problema de segurança de threads, e adicionar o lock só cria um problema de desempenho.

Daí surge o DCL.

4. Padrão double-checked locking

A versão acima adquire e libera o lock em qualquer situação, mas para operações de leitura não existe problema de segurança de threads; portanto, o lock só é necessário na primeira criação da instância, e nunca depois. Vejamos primeiro a implementação do DCLP:

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

Com base no exposto, podemos escrever double-checked locking + recuperação automática

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;
}

O ponto-chave do DCLP é que a maioria das chamadas a instance verá p como não nulo, de modo que nem sequer tentará inicializá-lo. Por isso, o DCLP testa se p é nulo antes de tentar adquirir o lock. Somente quando o teste é bem-sucedido (ou seja, p ainda não foi inicializado) ele adquire o lock e então verifica novamente se p ainda é nulo (daí o nome double-checked locking). A segunda verificação é necessária porque, como acabamos de ver, é bem possível que outra thread tenha inicializado p por acaso depois da primeira verificação e antes de conseguir o lock.

O código acima parece maravilhoso, mas depois de bastante tempo a falha foi descoberta. O motivo: execução fora de ordem de leituras e escritas de memória (um problema do compilador).

Consideremos novamente a linha que inicializa p:

p = new singleton;

Esta instrução provoca três coisas:

  1. Alocar memória capaz de armazenar o objeto singleton;
  2. Construir um objeto singleton na memória alocada;
  3. Fazer p apontar para essa memória alocada.

Pode-se pensar que esses três passos são executados em ordem, mas na verdade só se pode garantir que o passo 1 é executado primeiro; os passos 2 e 3, não necessariamente. O problema está exatamente aí.

  • A thread A chama instance, executa o primeiro teste de p, adquire o lock, executa os passos 1 e 3 e então é suspensa. Nesse momento p é não nulo, mas ainda nenhum objeto Singleton foi construído na memória para a qual p aponta.
  • A thread B chama instance, verifica que p é não nulo e o devolve ao chamador de instance. O chamador desreferencia o ponteiro para obter o singleton — opa, um objeto que ainda não foi construído. É aí que aparece o bug.

O DCLP funciona bem somente quando os passos um e dois são executados antes do passo três, mas não há como expressar essa restrição em C ou C++. É como um punhal cravado no coração do DCLP: precisamos definir restrições sobre a ordem relativa das instruções, mas nossa linguagem não oferece forma de expressar tais restrições.

5. A instrução memory barrier

O problema do DCLP foi resolvido no C++11.

Como o novo C++11 definiu um novo modelo de memória, garante-se que não haverá troca de threads durante a execução daqueles 3 passos — o processo de inicialização equivale a uma operação "atômica" — e o DCL pode ser usado corretamente de novo. Contudo, no C++11 existe uma forma ainda mais concisa de escrever um singleton multi-thread, que apresentaremos mais adiante.

Antes do C++11, a solução era a instrução barrier. Para que ela execute corretamente, é preciso colocar um memory barrier entre os passos 2 e 3, forçando a CPU a executar na ordem 1, 2, 3.

Primeira implementação:

Baseada em operator new + placement new, escrevendo o código na ordem de execução 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;
}

Segunda implementação:

Baseada na inserção direta da instrução ASM mfence; a macro barrier do unix também é implementada com essa instrução.

#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;
}

Normalmente chama-se uma instrução fornecida pela CPU; o papel dessa instrução é impedir que a CPU troque instruções anteriores a ela para depois dela, e por isso ela costuma ser chamada de barrier. No código acima, asm indica que é uma instrução assembly; volatile é opcional — se usado, declara ao compilador que não é permitido otimizar essa instrução assembly. lwsync é a instrução barrier fornecida pelo POWERPC.

6. Variáveis estáticas locais

Scott Meyers propôs, em Effective C++, uma forma concisa de escrever um singleton

singleton &singleton::instance() {
    static singleton p;
    return p;
}
  • Em uso single-threaded, correto.
  • Com múltiplas threads em C++11 e versões posteriores (como C++14), correto.
  • Com múltiplas threads antes do C++11, não necessariamente correto.

O motivo é que os padrões anteriores ao C++11 não definiam um modelo de memória para variáveis static locais; assim, não era thread-safe. Já no C++11 é thread-safe, porque o novo padrão C++ estabelece que, quando uma thread está inicializando uma variável, as demais threads devem aguardar a conclusão dessa inicialização para poder acessá-la.

As ordens de memória usadas acima:

  • memory_order_relaxed: ordem de memória relaxada, serve apenas para garantir que as operações sobre o objeto atômico sejam atômicas
  • memory_order_acquire: operação de aquisição; ao ler um objeto atômico, nenhuma operação de leitura ou escrita posterior da thread atual pode ser reordenada para antes desta operação, e todas as escritas de memória de outras threads anteriores à liberação do mesmo objeto atômico tornam-se visíveis para a thread atual
  • memory_order_release: operação de liberação; ao escrever em um objeto atômico, nenhuma operação de leitura ou escrita anterior da thread atual pode ser reordenada para depois desta operação, e todas as escritas de memória da thread atual tornam-se visíveis para as outras threads que adquirirem o mesmo objeto atômico

7. Atomic

Em versões anteriores ao C++11, além de implementar um Singleton thread-safe com locks, também é possível usar as atomic operation embutidas nos diversos compiladores.

Depois que java e c# descobriram o problema de reordenação, adicionaram uma palavra-chave volatile: ao declarar a variável p, é preciso qualificá-la com volatile, e o compilador, ao vê-lo, sabe que aqui não pode haver reordenação (primeiro alocar a memória, depois executar o construtor e só quando tudo estiver concluído fazer a atribuição).

Já o padrão do c++ nunca foi corrigido, então o VC++ também adicionou essa palavra-chave na versão 2005, mas ela não é multiplataforma (só oferece suporte a plataformas Microsoft).

E, chegando ao c++ 11, para eliminar essas falhas pela raiz, foi introduzido um modelo de memória adequado ao multithreading. Finalmente existe um mecanismo que nos ajuda a implementar uma solução multiplataforma.

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;
}

Vale notar que o código acima usa dois termos bastante fundamentais, aquisição e liberação:

  • A aquisição é uma operação de leitura da memória; nenhuma operação de leitura ou escrita posterior da thread atual pode ser reordenada para antes dela.
  • A liberação é uma operação de escrita na memória; nenhuma operação de leitura ou escrita anterior da thread atual pode ser reordenada para depois dela.

acquire e release normalmente aparecem em pares, com o objetivo de garantir que, se o release sobre o mesmo objeto atômico ocorrer antes do acquire, as modificações de memória anteriores ao release possam ser todas vistas pelas leituras de memória posteriores ao acquire.

8. pthread_once

Em plataformas unix, além de usar atomic operation, quando não se pode usar o C++11 também é possível implementar o Singleton com pthread_once.

O protótipo é o seguinte:

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

Implementação:

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. Resumo

Este artigo explicou vários padrões singleton, os padrões singleton thread-safe, e várias implementações thread-safe que não usam C++11: memory barrier, variáveis estáticas locais, a abordagem pthread_once, a implementação com atomic do C++11, etc.

Por fim, vale notar que, para o destruidor das classes singleton acima, consulte a implementação double-checked locking + recuperação automática: é preciso declarar uma variável estática local dentro da classe. Uma variável estática local pode ser entendida como uma variável global; ao término do programa, o destruidor dessa variável estática local é chamado automaticamente. É por isso que se declara e define uma classe aninhada dentro da classe, em vez de escrever diretamente o destruidor do singleton.

10. Referências

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

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

https://segmentfault.com/a/1190000015950693

Uma classe template singleton thread-safe reutilizável

Atendendo ao pedido do issue #303: seguindo a ideia do boost::container::singleton_default, uma classe base template CRTP pode transformar qualquer classe em um singleton thread-safe com uma linha (veja o código completo em singleton_default.hpp, com testes de concorrência incluídos):

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

Comentários