Ú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:
Esta instrução provoca três coisas:
- Alocar memória capaz de armazenar o objeto
singleton; - Construir um objeto
singletonna memória alocada; - Fazer
papontar 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
- 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:
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
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