A implementação do encadeamento de padrões Singleton por Meyers é segura?

145

A implementação a seguir, usando a inicialização lenta, do Singletonthread (Meyers 'Singleton) é segura?

static Singleton& instance()
{
     static Singleton s;
     return s;
}

Caso contrário, por que e como torná-lo seguro?

Ankur
fonte
Alguém pode explicar por que isso não é seguro para threads. Os artigos mencionados nos links discutem a segurança do encadeamento usando uma implementação alternativa (usando uma variável de ponteiro, ou seja, estática Singleton * pInstance).
Ankur

Respostas:

168

No C ++ 11 , é seguro para threads. De acordo com a norma , §6.7 [stmt.dcl] p4:

Se o controle digitar a declaração simultaneamente enquanto a variável estiver sendo inicializada, a execução simultânea deverá aguardar a conclusão da inicialização.

O suporte do GCC e do VS para o recurso ( Inicialização dinâmica e destruição com simultaneidade , também conhecida como estática mágica no MSDN ) é o seguinte:

Obrigado a @Mankarse e @olen_gam por seus comentários.


No C ++ 03 , esse código não era seguro para threads. Há um artigo de Meyers chamado "C ++ e os perigos do bloqueio com verificação dupla" que discute implementações seguras de threads do padrão, e a conclusão é, mais ou menos, que (no C ++ 03) o bloqueio completo em torno do método de instanciação é basicamente a maneira mais simples de garantir a simultaneidade adequada em todas as plataformas, enquanto a maioria das formas de variantes de padrão de bloqueio com verificação dupla podem sofrer condições de corrida em determinadas arquiteturas , a menos que as instruções sejam intercaladas com estrategicamente coloque barreiras de memória.

Groo
fonte
3
Também há uma extensa discussão sobre o Padrão Singleton (vida útil e segurança de thread) por Alexandrescu no Design C ++ Moderno. Veja o site do Loki: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Você pode criar um singleton seguro para threads com boost :: call_once.
precisa saber é
1
Infelizmente, essa parte do padrão não é implementada no Visual Studio 2012 C ++ Compiler. Referido como "Magic Statics" na tabela "C ++ 11 Core Language Features: Concurrency" aqui: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
O trecho do padrão trata da construção, mas não da destruição. O padrão evita que o objeto seja destruído em um thread enquanto (ou antes) outro thread tenta acessá-lo na finalização do programa?
stewbasic
IANA (linguagem C ++) L mas a seção 3.6.3 [basic.start.term] p2 sugere que é possível atingir um comportamento indefinido tentando acessar o objeto depois que ele foi destruído?
stewbasic
21

Para responder sua pergunta sobre por que não é seguro para threads, não é porque a primeira chamada para instance()deve chamar o construtor Singleton s. Para ser seguro para threads, isso teria que ocorrer em uma seção crítica, mas não há exigência no padrão de que uma seção crítica seja realizada (o padrão até o momento é completamente silencioso em threads). Os compiladores geralmente implementam isso usando uma verificação e incremento simples de um boolean estático - mas não em uma seção crítica. Algo como o seguinte pseudocódigo:

static Singleton& instance()
{
    static bool initialized = false;
    static char s[sizeof( Singleton)];

    if (!initialized) {
        initialized = true;

        new( &s) Singleton(); // call placement new on s to construct it
    }

    return (*(reinterpret_cast<Singleton*>( &s)));
}

Então, aqui está um simples Singleton seguro para threads (para Windows). Ele usa um invólucro de classe simples para o objeto CRITICAL_SECTION do Windows, para que possamos fazer com que o compilador inicialize automaticamente CRITICAL_SECTIONantes de main()ser chamado. Idealmente, seria usada uma verdadeira classe de seção crítica RAII que pode lidar com exceções que podem ocorrer quando a seção crítica é realizada, mas isso está além do escopo desta resposta.

A operação fundamental é que, quando uma instância de Singletoné solicitada, um bloqueio é realizado, o Singleton é criado, se necessário, o bloqueio é liberado e a referência do Singleton retornada.

#include <windows.h>

class CritSection : public CRITICAL_SECTION
{
public:
    CritSection() {
        InitializeCriticalSection( this);
    }

    ~CritSection() {
        DeleteCriticalSection( this);
    }

private:
    // disable copy and assignment of CritSection
    CritSection( CritSection const&);
    CritSection& operator=( CritSection const&);
};


class Singleton
{
public:
    static Singleton& instance();

private:
    // don't allow public construct/destruct
    Singleton();
    ~Singleton();
    // disable copy & assignment
    Singleton( Singleton const&);
    Singleton& operator=( Singleton const&);

    static CritSection instance_lock;
};

CritSection Singleton::instance_lock; // definition for Singleton's lock
                                      //  it's initialized before main() is called


Singleton::Singleton()
{
}


Singleton& Singleton::instance()
{
    // check to see if we need to create the Singleton
    EnterCriticalSection( &instance_lock);
    static Singleton s;
    LeaveCriticalSection( &instance_lock);

    return s;
}

Cara - isso é muita porcaria para "fazer uma melhor global".

As principais desvantagens dessa implementação (se eu não deixei passar alguns bugs) são:

  • se new Singleton()jogar, a trava não será liberada. Isso pode ser corrigido usando um objeto de bloqueio RAII verdadeiro, em vez do simples que tenho aqui. Isso também pode ajudar a tornar as coisas portáteis se você usar algo como o Boost para fornecer um invólucro independente da plataforma para o bloqueio.
  • isso garante a segurança do encadeamento quando a instância Singleton é solicitada após a main()chamada - se você a chamar antes (como na inicialização de um objeto estático), as coisas podem não funcionar porque CRITICAL_SECTIONpodem não ser inicializadas.
  • um bloqueio deve ser executado toda vez que uma instância é solicitada. Como eu disse, esta é uma implementação segura de thread simples. Se você precisar de uma melhor (ou quiser saber por que coisas como a técnica de trava de verificação dupla são falhas), consulte os papéis vinculados na resposta do Groo .
Michael Burr
fonte
1
Ah, oh. O que acontece se new Singleton()joga?
Sbi #
@ Bob - para ser justo, com um conjunto adequado de bibliotecas, todo o lixo relacionado à não-cópia e a um bloqueio RAII adequado desapareceriam ou seriam mínimos. Mas eu queria que o exemplo fosse razoavelmente independente. Embora os singletons sejam muito trabalhosos para um ganho talvez mínimo, eu os achei úteis no gerenciamento do uso de globais. Eles tendem a facilitar a descoberta de onde e quando são usados ​​um pouco melhor do que apenas uma convenção de nomenclatura.
227 Michael Burr
@sbi: neste exemplo, se new Singleton()joga, definitivamente há um problema com a trava. Uma classe de bloqueio RAII adequada deve ser usada, algo como o lock_guardBoost. Queria que o exemplo fosse mais ou menos independente, e já era um monstro, então deixei de lado a segurança de exceção (mas o chamei). Talvez eu deva corrigir isso para que esse código não seja cortado e colado em algum lugar inadequado.
227 Michael Burr
Por que alocar dinamicamente o singleton? Por que não fazer apenas 'pInstance' um membro estático de 'Singleton :: instance ()'?
Martin Iorque
@ Martin - feito. Você está certo, isso simplifica um pouco - seria ainda melhor se eu usasse uma classe de bloqueio RAII.
22711 Michael Burr
10

Observando o próximo padrão (seção 6.7.4), ele explica como a inicialização local estática é segura para threads. Portanto, uma vez que essa seção do padrão seja amplamente implementada, o Singleton da Meyer será a implementação preferida.

Eu discordo de muitas respostas já. A maioria dos compiladores já implementa a inicialização estática dessa maneira. A única exceção notável é o Microsoft Visual Studio.

deft_code
fonte
6

A resposta correta depende do seu compilador. Ele pode decidir torná- lo mais seguro; não é "naturalmente" thread-safe.

MSalters
fonte
5

O seguinte thread de implementação [...] é seguro?

Na maioria das plataformas, isso não é seguro para threads. (Anexe o aviso de isenção de responsabilidade usual, explicando que o padrão C ++ não sabe sobre threads, portanto, legalmente, não diz se é ou não.)

Se não, por que [...]?

O motivo é que nada impede que mais de um thread execute simultaneamente so construtor.

como torná-lo seguro thread?

"C ++ e os perigos do bloqueio com dupla verificação", de Scott Meyers e Andrei Alexandrescu, é um ótimo tratado sobre o assunto de singletons seguros para threads.

sbi
fonte
2

Como o MSalters disse: Depende da implementação do C ++ usada. Verifique a documentação. Quanto à outra pergunta: "Se não, por quê?" - O padrão C ++ ainda não menciona nada sobre threads. Mas a próxima versão do C ++ está ciente de threads e afirma explicitamente que a inicialização de locais estáticos é segura para threads. Se dois threads chamam essa função, um thread executará uma inicialização enquanto o outro bloqueará e aguardará o término.

sellibitze
fonte