L'implémentation par Meyers du thread de modèle Singleton est-elle sûre?

145

L'implémentation suivante, utilisant l'initialisation différée, de Singleton(Meyers 'Singleton) est-elle sûre?

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

Sinon, pourquoi et comment le rendre sûr pour les threads?

Ankur
la source
Quelqu'un peut-il expliquer pourquoi ce n'est pas thread-safe. Les articles mentionnés dans les liens discutent de la sécurité des threads en utilisant une implémentation alternative (en utilisant une variable de pointeur ie statique Singleton * pInstance).
Ankur
Copie
Trevor Boyd Smith

Réponses:

168

En C ++ 11 , il est thread-safe. Selon la norme , §6.7 [stmt.dcl] p4:

Si le contrôle entre la déclaration simultanément pendant l'initialisation de la variable, l' exécution simultanée doit attendre la fin de l'initialisation.

La prise en charge de GCC et VS pour la fonctionnalité ( Initialisation et Destruction Dynamiques avec Concurrence , également connue sous le nom de Magic Statics sur MSDN ) est la suivante:

Merci à @Mankarse et @olen_gam pour leurs commentaires.


En C ++ 03 , ce code n'était pas thread-safe. Il existe un article de Meyers intitulé "C ++ and the Perils of Double-Checked Locking" qui traite des implémentations thread-safe du modèle, et la conclusion est, plus ou moins, que (en C ++ 03) un verrouillage complet autour de la méthode d'instanciation est fondamentalement le moyen le plus simple d'assurer une concurrence appropriée sur toutes les plates-formes, tandis que la plupart des formes de variantes de modèle de verrouillage vérifiées deux fois peuvent souffrir de conditions de concurrence sur certaines architectures , à moins que les instructions ne soient entrelacées avec des barrières de mémoire placées stratégiquement.

Groo
la source
3
Il y a également une discussion approfondie sur le modèle Singleton (durée de vie et sécurité des threads) par Alexandrescu dans la conception C ++ moderne. Voir le site de Loki: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Vous pouvez créer un singleton thread-safe avec boost :: call_once.
CashCow
1
Malheureusement, cette partie de la norme n'est pas implémentée dans le compilateur C ++ Visual Studio 2012. Appelé «Magic Statics» dans le tableau «C ++ 11 Core Language Features: Concurrency» ici: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
L'extrait de la norme concerne la construction mais pas la destruction. La norme empêche-t-elle l'objet d'être détruit sur un thread pendant (ou avant) un autre thread tente d'y accéder à la fin du programme?
stewbasic
IANA (langage C ++) L mais la section 3.6.3 [basic.start.term] p2 suggère qu'il est possible d'atteindre un comportement indéfini en essayant d'accéder à l'objet après qu'il a été détruit?
stewbasic
21

Pour répondre à votre question sur la raison pour laquelle ce n'est pas threadsafe, ce n'est pas parce que le premier appel à instance()doit appeler le constructeur pour Singleton s. Pour être threadsafe, cela devrait se produire dans une section critique, mais il n'y a pas d'exigence dans la norme qu'une section critique soit prise (la norme à ce jour est complètement silencieuse sur les threads). Les compilateurs implémentent souvent cela en utilisant une simple vérification et incrémentation d'un booléen statique - mais pas dans une section critique. Quelque chose comme le pseudocode suivant:

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

Voici donc un simple Singleton thread-safe (pour Windows). Il utilise une simple enveloppe de classe pour l'objet de Windows CRITICAL_SECTION afin que nous puissions avoir le compilateur initialiser automatiquement l' CRITICAL_SECTIONavant main()est appelée. Idéalement, une véritable classe de section critique RAII serait utilisée pour traiter les exceptions qui pourraient se produire lorsque la section critique est conservée, mais cela dépasse le cadre de cette réponse.

L'opération fondamentale est que lorsqu'une instance de Singletonest demandée, un verrou est pris, le singleton est créé s'il le faut, puis le verrou est libéré et la référence Singleton renvoyée.

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

Mec - c'est beaucoup de merde pour "faire un monde meilleur".

Les principaux inconvénients de cette implémentation (si je n'ai pas laissé passer certains bogues) sont:

  • si new Singleton()jette, le verrou ne sera pas libéré. Cela peut être résolu en utilisant un véritable objet de verrouillage RAII au lieu du simple objet que j'ai ici. Cela peut également aider à rendre les choses portables si vous utilisez quelque chose comme Boost pour fournir un wrapper indépendant de la plate-forme pour le verrou.
  • cela garantit la sécurité des threads lorsque l'instance Singleton est demandée après avoir main()été appelée - si vous l'appelez avant (comme dans l'initialisation d'un objet statique), les choses peuvent ne pas fonctionner car le CRITICAL_SECTIONpeut ne pas être initialisé.
  • un verrou doit être pris à chaque fois qu'une instance est demandée. Comme je l'ai dit, il s'agit d'une implémentation simple thread safe. Si vous avez besoin d'un meilleur (ou si vous voulez savoir pourquoi des choses comme la technique du double-contrôle de verrouillage sont imparfaites), consultez les articles liés à la réponse de Groo .
Michael Burr
la source
1
Oh oh. Que se passe-t-il si new Singleton()jette?
sbi
@Bob - pour être honnête, avec un ensemble approprié de bibliothèques, tout ce qui a trait à la non-copiable et à un verrou RAII approprié disparaîtrait ou serait minime. Mais je voulais que l'exemple soit raisonnablement autonome. Même si les singleton représentent beaucoup de travail pour un gain peut-être minime, je les ai trouvés utiles pour gérer l'utilisation des globals. Ils ont tendance à rendre plus facile de savoir où et quand ils sont utilisés, un peu mieux qu'une simple convention de dénomination.
Michael Burr
@sbi: dans cet exemple, si new Singleton()jette il y a certainement un problème avec le verrou. Une classe de verrouillage RAII appropriée doit être utilisée, quelque chose comme lock_guardBoost. Je voulais que l'exemple soit plus ou moins autonome, et c'était déjà un peu un monstre, alors j'ai laissé la sécurité des exceptions (mais je l'ai appelé). Peut-être que je devrais résoudre ce problème pour que ce code ne soit pas copié-collé dans un endroit inapproprié.
Michael Burr
Pourquoi allouer dynamiquement le singleton? Pourquoi ne pas simplement faire de 'pInstance' un membre statique de 'Singleton :: instance ()'?
Martin York
@Martin - c'est fait. Vous avez raison, cela rend les choses un peu plus simples - ce serait encore mieux si j'utilisais une classe de verrouillage RAII.
Michael Burr
10

En regardant le standard suivant (section 6.7.4), il explique comment l'initialisation locale statique est thread-safe. Ainsi, une fois que cette section de la norme sera largement implémentée, Meyer's Singleton sera l'implémentation préférée.

Je suis déjà en désaccord avec de nombreuses réponses. La plupart des compilateurs implémentent déjà l'initialisation statique de cette façon. La seule exception notable est Microsoft Visual Studio.

deft_code
la source
6

La bonne réponse dépend de votre compilateur. Il peut décider de le rendre threadsafe; ce n'est pas "naturellement" threadsafe.

MSalters
la source
5

L'implémentation suivante est-elle [...] thread-safe?

Sur la plupart des plates-formes, ce n'est pas thread-safe. (Ajoutez la clause de non-responsabilité habituelle expliquant que le standard C ++ ne connaît pas les threads, donc, légalement, il ne dit pas si c'est le cas ou non.)

Sinon, pourquoi [...]?

La raison pour laquelle ce n'est pas le cas est que rien n'empêche plus d'un thread d'exécuter simultanément s'constructeur.

comment le rendre sûr pour les threads?

"C ++ et les périls du double verrouillage" par Scott Meyers et Andrei Alexandrescu est un très bon traité sur le sujet des singletons thread-safe.

sbi
la source
2

Comme MSalters l'a dit: cela dépend de l'implémentation C ++ que vous utilisez. Consultez la documentation. Quant à l'autre question: "Si non, pourquoi?" - Le standard C ++ ne mentionne encore rien sur les threads. Mais la prochaine version C ++ est consciente des threads et indique explicitement que l'initialisation des locals statiques est thread-safe. Si deux threads appellent une telle fonction, un thread effectuera une initialisation tandis que l'autre bloquera et attendra qu'elle se termine.

sellibitze
la source