Pergunta

Existe uma maneira de implementar um objeto singleton em C++ que seja:

  1. Construído preguiçosamente de maneira segura para threads (dois threads podem ser simultaneamente o primeiro usuário do singleton - ele ainda deve ser construído apenas uma vez).
  2. Não depende de variáveis ​​estáticas sendo construídas previamente (portanto, o objeto singleton é seguro para uso durante a construção de variáveis ​​estáticas).

(Eu não conheço meu C++ bem o suficiente, mas será que variáveis ​​estáticas integrais e constantes são inicializadas antes de qualquer código ser executado (ou seja, mesmo antes de construtores estáticos serem executados - seus valores já podem estar "inicializados" no programa imagem)?Se assim for - talvez isso possa ser explorado para implementar um mutex singleton - que por sua vez pode ser usado para proteger a criação do singleton real.)


Excelente, parece que agora tenho algumas respostas boas (pena que não posso marcar 2 ou 3 como sendo a resposta).Parece haver duas soluções amplas:

  1. Use a inicialização estática (em oposição à inicialização dinâmica) de uma variável estática POD e implemente meu próprio mutex usando as instruções atômicas integradas.Esse era o tipo de solução que eu estava sugerindo na minha pergunta e acredito que já sabia.
  2. Use alguma outra função de biblioteca como pthread_once ou impulsionar::chamar_once.Eu certamente não conhecia essas coisas - e estou muito grato pelas respostas postadas.
Foi útil?

Solução

Basicamente, você está solicitando a criação sincronizada de um singleton, sem usar nenhuma sincronização (variáveis ​​​​previamente construídas).Em geral, não, isso não é possível.Você precisa de algo disponível para sincronização.

Quanto à sua outra pergunta, sim, variáveis ​​estáticas que podem ser inicializadas estaticamente (ou seja,nenhum código de tempo de execução necessário) têm garantia de inicialização antes que outro código seja executado.Isso torna possível usar um mutex inicializado estaticamente para sincronizar a criação do singleton.

Da revisão de 2003 do padrão C++:

Objetos com duração de armazenamento estático (3.7.1) devem ser inicializados com zero (8.5) antes que qualquer outra inicialização ocorra.A inicialização zero e a inicialização com uma expressão constante são chamadas coletivamente de inicialização estática;todas as outras inicializações são inicializações dinâmicas.Objetos dos tipos POD (3.9) com duração de armazenamento estático inicializados com expressões constantes (5.19) devem ser inicializados antes que qualquer inicialização dinâmica ocorra.Objetos com duração de armazenamento estático definida no escopo do namespace na mesma unidade de tradução e inicializados dinamicamente devem ser inicializados na ordem em que sua definição aparece na unidade de tradução.

Se você saber que você usará esse singleton durante a inicialização de outros objetos estáticos, acho que você descobrirá que a sincronização não é um problema.Até onde sei, todos os principais compiladores inicializam objetos estáticos em um único thread, portanto, segurança de thread durante a inicialização estática.Você pode declarar seu ponteiro singleton como NULL e verificar se ele foi inicializado antes de usá-lo.

No entanto, isso pressupõe que você saber que você usará esse singleton durante a inicialização estática.Isso também não é garantido pelo padrão, portanto, se você quiser estar completamente seguro, use um mutex inicializado estaticamente.

Editar:A sugestão de Chris de usar uma comparação e troca atômica certamente funcionaria.Se a portabilidade não for um problema (e a criação de singletons temporários adicionais não for um problema), então é uma solução com sobrecarga um pouco menor.

Outras dicas

Infelizmente, a resposta de Matt apresenta o que é chamado bloqueio verificado duas vezes que não é compatível com o modelo de memória C/C++.(Ele é suportado pelo modelo de memória Java 1.5 e posterior - e acho que .NET.) Isso significa que entre o momento em que o pObj == NULL a verificação ocorre e quando o bloqueio (mutex) é adquirido, pObj pode já ter sido atribuído em outro thread.A troca de threads acontece sempre que o sistema operacional deseja, não entre as "linhas" de um programa (que não têm significado após a compilação na maioria dos idiomas).

Além disso, como Matt reconhece, ele usa uma int como um bloqueio em vez de um sistema operacional primitivo.Não faça isso.Bloqueios adequados requerem o uso de instruções de barreira de memória, potencialmente liberações de linha de cache e assim por diante;use as primitivas do seu sistema operacional para bloqueio.Isto é especialmente importante porque as primitivas usadas podem mudar entre as linhas individuais da CPU nas quais seu sistema operacional é executado;o que funciona em uma CPU Foo pode não funcionar em uma CPU Foo2.A maioria dos sistemas operacionais oferece suporte nativo a threads POSIX (pthreads) ou os oferece como um wrapper para o pacote de threading do sistema operacional, portanto, geralmente é melhor ilustrar exemplos usando-os.

Se o seu sistema operacional oferece primitivas apropriadas, e se você realmente precisa delas para desempenho, em vez de fazer esse tipo de bloqueio/inicialização, você pode usar um comparação e troca atômica operação para inicializar uma variável global compartilhada.Essencialmente, o que você escreverá será assim:

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

Isso só funciona se for seguro criar várias instâncias do seu singleton (uma por thread que invoca GetSingleton() simultaneamente) e depois descartar os extras.O OSAtomicCompareAndSwapPtrBarrier função fornecida no Mac OS X — a maioria dos sistemas operacionais fornece uma primitiva semelhante — verifica se pObj é NULL e apenas realmente o define como temp para isso, se for.Isso usa suporte de hardware para realmente, literalmente, realizar apenas a troca uma vez e diga se isso aconteceu.

Outra facilidade a ser aproveitada se o seu sistema operacional oferecer algo entre esses dois extremos é pthread_once.Isso permite configurar uma função que é executada apenas uma vez - basicamente fazendo todo o bloqueio/barreira/etc.truques para você - não importa quantas vezes ele seja invocado ou em quantos threads ele seja invocado.

Aqui está um getter singleton muito simples e preguiçosamente construído:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

Isso é preguiçoso e o próximo padrão C++ (C++0x) exige que ele seja thread-safe.Na verdade, acredito que pelo menos o g++ implementa isso de maneira segura para threads.Então, se esse é o seu compilador de destino ou se você usar um compilador que também implemente isso de maneira segura para threads (talvez os compiladores mais recentes do Visual Studio o façam?Não sei), então isso pode ser tudo que você precisa.

Veja também http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html neste tópico.

Você não pode fazer isso sem variáveis ​​estáticas, no entanto, se estiver disposto a tolerar uma, você pode usar Impulsionar.Thread para este propósito.Leia a seção "inicialização única" para obter mais informações.

Então, na sua função de acessador singleton, use boost::call_once para construir o objeto e devolvê-lo.

Para o gcc, isso é bastante fácil:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

O GCC garantirá que a inicialização seja atômica. Para VC++, este não é o caso. :-(

Um grande problema com este mecanismo é a falta de testabilidade:se você precisar redefinir o LazyType para um novo entre os testes ou quiser alterar o LazyType* para um MockLazyType*, não será possível.Diante disso, geralmente é melhor usar um mutex estático + ponteiro estático.

Além disso, possivelmente um aparte:É melhor sempre evitar tipos não-POD estáticos.(Indicações para PODs são aceitáveis.) As razões para isso são muitas:como você mencionou, a ordem de inicialização não está definida - nem a ordem em que os destruidores são chamados.Por causa disso, os programas acabarão travando ao tentar sair;muitas vezes não é grande coisa, mas às vezes é um empecilho quando o criador de perfil que você está tentando usar requer uma saída limpa.

Embora esta pergunta já tenha sido respondida, acho que há alguns outros pontos a serem mencionados:

  • Se você deseja uma instanciação lenta do singleton ao usar um ponteiro para uma instância alocada dinamicamente, você terá que limpá-lo no ponto certo.
  • Você poderia usar a solução de Matt, mas precisaria usar uma seção mutex/crítica adequada para bloqueio e verificar "pObj == NULL" antes e depois do bloqueio.Claro, pObj também teria que ser estático ;).Um mutex seria desnecessariamente pesado neste caso, seria melhor usar uma seção crítica.

Mas, como já foi dito, você não pode garantir uma inicialização lenta segura para threads sem usar pelo menos uma primitiva de sincronização.

Editar:Sim, Derek, você está certo.Meu erro.:)

Você poderia usar a solução de Matt, mas precisaria usar uma seção mutex/crítica adequada para bloqueio e verificar "pObj == NULL" antes e depois do bloqueio.Claro, pObj também teria que ser estático;).Um mutex seria desnecessariamente pesado neste caso, seria melhor usar uma seção crítica.

JO, isso não funciona.Como Chris apontou, isso é um bloqueio de verificação dupla, que não tem garantia de funcionar no padrão C++ atual.Ver: C++ e os perigos do bloqueio verificado duas vezes

Editar:Não tem problema, JO.É muito bom em idiomas onde funciona.Espero que funcione em C++ 0x (embora não tenha certeza), porque é um idioma muito conveniente.

  1. leia no modelo de memória fraca.Ele pode quebrar bloqueios e spinlocks verificados duas vezes.Intel é um modelo de memória forte (ainda), então na Intel é mais fácil

  2. use cuidadosamente "volátil" para evitar o cache de partes do objeto nos registros, caso contrário você terá inicializado o ponteiro do objeto, mas não o objeto em si, e o outro thread irá travar

  3. a ordem de inicialização de variáveis ​​estáticas versus carregamento de código compartilhado às vezes não é trivial.Já vi casos em que o código para destruir um objeto já estava descarregado, então o programa travou ao sair

  4. tais objetos são difíceis de destruir adequadamente

Em geral, singletons são difíceis de fazer corretamente e de depurar.É melhor evitá-los completamente.

Suponho que estou dizendo para não fazer isso porque não é seguro e provavelmente irá quebrar com mais frequência do que apenas inicializar essas coisas no main() não será tão popular.

(E sim, eu sei que sugerir isso significa que você não deve tentar fazer coisas interessantes em construtores de objetos globais.Essa é a questão.)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top