Question

Existe-t-il un moyen d'implémenter un objet singleton en C++ :

  1. Construit paresseusement de manière thread-safe (deux threads peuvent être simultanément le premier utilisateur du singleton - il ne doit toujours être construit qu'une seule fois).
  2. Ne repose pas sur la construction préalable de variables statiques (l'objet singleton peut donc lui-même être utilisé en toute sécurité lors de la construction de variables statiques).

(Je ne connais pas assez bien mon C++, mais est-il vrai que les variables statiques intégrales et constantes sont initialisées avant l'exécution de tout code (c'est-à-dire avant même l'exécution des constructeurs statiques - leurs valeurs peuvent déjà être "initialisées" dans le programme image)?Si tel est le cas - cela peut peut-être être exploité pour implémenter un mutex singleton - qui peut à son tour être utilisé pour protéger la création du véritable singleton..)


Excellent, il semble que j'ai quelques bonnes réponses maintenant (dommage que je ne puisse pas marquer 2 ou 3 comme étant la réponse).Il semble y avoir deux grandes solutions :

  1. Utilisez l'initialisation statique (par opposition à l'initialisation dynamique) d'une variable statique POD et implémentez mon propre mutex avec celui-ci en utilisant les instructions atomiques intégrées.C'était le type de solution à laquelle je faisais allusion dans ma question, et je crois que je le savais déjà.
  2. Utilisez une autre fonction de bibliothèque comme pthread_once ou boost :: call_once.Je ne les connaissais certainement pas - et je suis très reconnaissant pour les réponses publiées.
Était-ce utile?

La solution

Fondamentalement, vous demandez une création synchronisée d'un singleton, sans utiliser aucune synchronisation (variables précédemment construites).En général, non, ce n'est pas possible.Vous avez besoin de quelque chose de disponible pour la synchronisation.

Quant à votre autre question, oui, des variables statiques qui peuvent être initialisées statiquement (c'est-à-direaucun code d'exécution nécessaire) sont garantis d'être initialisés avant l'exécution d'un autre code.Cela permet d'utiliser un mutex initialisé statiquement pour synchroniser la création du singleton.

De la révision de 2003 de la norme C++ :

Les objets avec une durée de stockage statique (3.7.1) doivent être initialisés à zéro (8.5) avant toute autre initialisation.L'initialisation nulle et l'initialisation avec une expression constante sont collectivement appelées initialisation statique ;toute autre initialisation est une initialisation dynamique.Les objets de types POD (3.9) avec une durée de stockage statique initialisée avec des expressions constantes (5.19) doivent être initialisés avant toute initialisation dynamique.Les objets dont la durée de stockage statique est définie dans la portée de l'espace de noms dans la même unité de traduction et initialisés dynamiquement doivent être initialisés dans l'ordre dans lequel leur définition apparaît dans l'unité de traduction.

Si tu savoir que vous utiliserez ce singleton lors de l'initialisation d'autres objets statiques, je pense que vous constaterez que la synchronisation n'est pas un problème.Au meilleur de ma connaissance, tous les principaux compilateurs initialisent les objets statiques dans un seul thread, donc la sécurité des threads lors de l'initialisation statique.Vous pouvez déclarer votre pointeur singleton comme étant NULL, puis vérifier s'il a été initialisé avant de l'utiliser.

Cependant, cela suppose que vous savoir que vous utiliserez ce singleton lors de l'initialisation statique.Ceci n'est pas non plus garanti par la norme, donc si vous voulez être totalement en sécurité, utilisez un mutex initialisé statiquement.

Modifier:La suggestion de Chris d'utiliser une comparaison et un échange atomique fonctionnerait certainement.Si la portabilité n'est pas un problème (et la création de singletons temporaires supplémentaires n'est pas un problème), il s'agit alors d'une solution légèrement inférieure.

Autres conseils

Malheureusement, la réponse de Matt présente ce qu'on appelle verrouillage revérifié qui n'est pas pris en charge par le modèle de mémoire C/C++.(Il est pris en charge par le modèle de mémoire Java 1.5 et versions ultérieures - et je pense .NET -.) Cela signifie qu'entre le moment où le pObj == NULL le contrôle a lieu et lorsque le verrou (mutex) est acquis, pObj peut avoir déjà été attribué sur un autre fil de discussion.Le changement de thread se produit chaque fois que le système d'exploitation le souhaite, et non entre les "lignes" d'un programme (qui n'ont aucune signification après la compilation dans la plupart des langages).

De plus, comme Matt le reconnaît, il utilise un int comme un verrou plutôt que comme une primitive du système d'exploitation.Ne fais pas ça.Des verrous appropriés nécessitent l'utilisation d'instructions de barrière de mémoire, potentiellement des vidages de ligne de cache, etc. ;utilisez les primitives de votre système d'exploitation pour le verrouillage.Ceci est particulièrement important car les primitives utilisées peuvent changer entre les lignes de processeur individuelles sur lesquelles votre système d'exploitation s'exécute ;ce qui fonctionne sur un CPU Foo peut ne pas fonctionner sur CPU Foo2.La plupart des systèmes d'exploitation prennent en charge nativement les threads POSIX (pthreads) ou les proposent comme wrapper pour le package de threads du système d'exploitation. Il est donc souvent préférable d'illustrer des exemples les utilisant.

Si votre système d'exploitation propose des primitives appropriées, et si vous en avez absolument besoin pour des performances, au lieu d'effectuer ce type de verrouillage/initialisation, vous pouvez utiliser un comparaison et échange atomiques opération pour initialiser une variable globale partagée.En gros, ce que vous écrivez ressemblera à ceci :

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

Cela ne fonctionne que s'il est sûr de créer plusieurs instances de votre singleton (une par thread qui invoque GetSingleton() simultanément), puis de jeter les extras.Le OSAtomicCompareAndSwapPtrBarrier fonction fournie sur Mac OS X — la plupart des systèmes d'exploitation fournissent une primitive similaire — vérifie si pObj est NULL et ne le définit que sur temp à lui si c'est le cas.Cela utilise le support matériel pour réellement, littéralement, effectuer uniquement l'échange. une fois et dites si c'est arrivé.

Une autre fonctionnalité à exploiter si votre système d'exploitation le propose et qui se situe entre ces deux extrêmes est pthread_once.Cela vous permet de configurer une fonction qui n'est exécutée qu'une seule fois - essentiellement en effectuant tout le verrouillage/barrière/etc.une supercherie pour vous - peu importe le nombre de fois où il est invoqué ou le nombre de threads sur lequel il est invoqué.

Voici un getter singleton très simple, construit paresseusement :

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

C'est paresseux, et la prochaine norme C++ (C++0x) exige qu'il soit thread-safe.En fait, je crois qu'au moins g++ implémente cela de manière thread-safe.Donc, si c'est votre compilateur cible ou si vous utilisez un compilateur qui l'implémente également de manière thread-safe (peut-être que les compilateurs Visual Studio plus récents le font ?Je ne sais pas), alors c'est peut-être tout ce dont vous avez besoin.

Regarde aussi http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html sur ce sujet.

Vous ne pouvez pas le faire sans variables statiques, mais si vous êtes prêt à en tolérer une, vous pouvez utiliser Boost.Thread dans ce but.Lisez la section "initialisation unique" pour plus d'informations.

Ensuite, dans votre fonction d'accesseur singleton, utilisez boost::call_once pour construire l'objet et le renvoyer.

Pour gcc, c'est plutôt simple :

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

GCC s'assurera que l'initialisation est atomique. Pour VC++, ce n'est pas le cas. :-(

Un problème majeur avec ce mécanisme est le manque de testabilité :si vous devez réinitialiser le LazyType à un nouveau entre les tests, ou si vous souhaitez changer le LazyType* en MockLazyType*, vous ne pourrez pas le faire.Compte tenu de cela, il est généralement préférable d’utiliser un mutex statique + un pointeur statique.

Aussi, peut-être un aparté :Il est préférable de toujours éviter les types statiques non POD.(Les pointeurs vers les POD sont acceptables.) Les raisons en sont nombreuses :comme vous le mentionnez, l'ordre d'initialisation n'est pas défini - ni l'ordre dans lequel les destructeurs sont appelés.Pour cette raison, les programmes finiront par planter lorsqu'ils tenteront de se fermer ;souvent, ce n'est pas grave, mais parfois un obstacle lorsque le profileur que vous essayez d'utiliser nécessite une sortie propre.

Bien que cette question ait déjà reçu une réponse, je pense qu'il y a quelques autres points à mentionner :

  • Si vous souhaitez une instanciation paresseuse du singleton tout en utilisant un pointeur vers une instance allouée dynamiquement, vous devrez vous assurer de le nettoyer au bon moment.
  • Vous pouvez utiliser la solution de Matt, mais vous devrez utiliser une section mutex/critique appropriée pour le verrouillage, et en cochant "pObj == NULL" avant et après le verrouillage.Bien sûr, pObj il faudrait aussi qu'il soit statique ;).Un mutex serait inutilement lourd dans ce cas, vous feriez mieux d'opter pour une section critique.

Mais comme déjà indiqué, vous ne pouvez pas garantir une initialisation paresseuse threadsafe sans utiliser au moins une primitive de synchronisation.

Modifier:Ouais Derek, tu as raison.Ma faute.:)

Vous pouvez utiliser la solution de Matt, mais vous devrez utiliser une section mutex/critique appropriée pour le verrouillage, et en cochant "pObj == NULL" avant et après le verrouillage.Bien sûr, pObj devrait également être statique ;) .Un mutex serait inutilement lourd dans ce cas, vous feriez mieux d'opter pour une section critique.

JO, ça ne marche pas.Comme Chris l'a souligné, il s'agit d'un verrouillage à double vérification, dont le fonctionnement n'est pas garanti dans la norme C++ actuelle.Voir: C++ et les dangers du verrouillage à double vérification

Modifier:Pas de problème, JO.C'est vraiment sympa dans les langues où ça marche.Je m'attends à ce que cela fonctionne en C++0x (même si je n'en suis pas certain), car c'est un idiome tellement pratique.

  1. lire sur un modèle de mémoire faible.Il peut briser les serrures et les spinlocks vérifiés deux fois.Intel est (encore) un modèle de mémoire puissant, donc sur Intel, c'est plus facile

  2. utilisez soigneusement "volatile" pour éviter de mettre en cache des parties de l'objet dans des registres, sinon vous aurez initialisé le pointeur d'objet, mais pas l'objet lui-même, et l'autre thread plantera

  3. l'ordre d'initialisation des variables statiques par rapport au chargement du code partagé n'est parfois pas trivial.J'ai vu des cas où le code pour détruire un objet était déjà déchargé, donc le programme plantait à la sortie

  4. de tels objets sont difficiles à détruire correctement

En général, les singletons sont difficiles à faire correctement et difficiles à déboguer.Il vaut mieux les éviter complètement.

Je suppose que dire de ne pas faire ça parce que ce n'est pas sûr et qu'il se cassera probablement plus souvent que de simplement initialiser ce truc dans main() ne sera pas si populaire.

(Et oui, je sais que suggérer cela signifie que vous ne devriez pas essayer de faire des choses intéressantes dans les constructeurs d'objets globaux.C'est le but.)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top