29 votes

Construction paresseuse thread-safe d'un singleton en C ++

Est-il un moyen de mettre en œuvre un singleton objet en C++, qui est:

  1. Paresseusement construit dans un thread de manière sûre (deux threads peut simultanément être le premier utilisateur de singleton - il devrait être construit à la fois).
  2. Ne pas compter sur les variables statiques sont en cours de construction à l'avance (donc l'objet singleton est même sûr à utiliser lors de la construction des variables statiques).

(Je ne sais pas mon C++ assez bien, mais est-ce le cas que l'intégrale et constante statique des variables sont initialisées avant tout code qui est exécuté (c'est à dire, avant même de constructeurs statiques sont exécutés leurs valeurs peuvent déjà être "initialisé" dans le programme de l'image)? Si donc - peut-être cela peut être exploité pour mettre en œuvre un singleton mutex qui peut à son tour être utilisé pour la garde de la création de la véritable singleton..)


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

  1. Utilisation statique d'initialisation (par opposition à la dynamique de l'initialisation) d'une GOUSSE variable statique, et de mettre mon propre mutex avec l'aide de la builtin atomique instructions. C'est le type de solution que j'ai été en faisant allusion à ma question, et je crois que je savais déjà.
  2. Utiliser une autre fonction de la bibliothèque comme pthread_once ou boost::call_once. Ces certes, je ne connaissais pas - et je suis très reconnaissant pour les réponses postées.

13voto

Chris Hanson Points 34485

Malheureusement, la réponse de Matt caractéristiques de ce qu'on appelle une double vérification de verrouillage , ce qui n'est pas pris en charge par le C/C++ modèle de mémoire. (Il est pris en charge par le Java 1.5 et les versions ultérieures - et je pense .NET - modèle de mémoire.) Cela signifie qu'entre le moment où l' pObj == NULL contrôle a lieu et lorsque le verrou (mutex) est acquis, pObj ont déjà été affecté sur un autre thread. Fil de commutation se produit chaque fois que l'OS le veut, et non pas entre les "lignes" d'un programme (qui n'ont pas de sens post-compilation, dans la plupart des langues).

En outre, comme Matt reconnaît, il utilise un int qu'un verrou plutôt qu'un OS primitif. Ne pas le faire. Bon verrous nécessitent l'utilisation de la barrière de mémoire d'instructions, potentiellement cache-ligne bouffées de chaleur, et ainsi de suite; l'utilisation de votre système d'exploitation primitives pour le verrouillage. Ceci est particulièrement important parce que les primitives utilisées peuvent changer entre chaque PROCESSEUR lignes que votre système d'exploitation s'exécute sur l'; ce qui fonctionne sur un PROCESSEUR Foo pourrait ne pas fonctionner sur CPU Foo2. La plupart des systèmes d'exploitation soit en charge en mode natif des threads POSIX (pthreads) ou de leur offrir comme un wrapper pour l'OS filetage paquet, de sorte qu'il est souvent préférable d'illustrer des exemples de leur utilisation.

Si votre système d'exploitation offre appropriée primitives, et si vous avez absolument besoin pour la performance, au lieu de faire ce type de verrouillage/initialisation vous pouvez utiliser un atomique de comparer et de swap de fonctionnement pour initialiser une variable globale partagée. Essentiellement, ce que vous écrivez va ressembler à 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 si il est sûr de créer plusieurs instances de votre singleton (une par fil qui arrive à invoquer GetSingleton() en même temps), et puis jetez-les extras loin. L' OSAtomicCompareAndSwapPtrBarrier fonction sur Mac OS X - la plupart des systèmes d'exploitation similaires primitive - vérifie si pObj est NULL et ne la définit temp si elle est. Il utilise du matériel de soutien pour vraiment, littéralement, à seulement effectuer la permutation d' une fois et dire que ce qui s'est passé.

Un autre établissement de l'effet de levier si votre système d'exploitation à l'offre qui est entre ces deux extrêmes est - pthread_once. Cela vous permet de définir une fonction qui est exécutée une seule fois - pour l'essentiel en faisant tout de verrouillage/barrière/etc. la ruse pour vous - peu importe combien de fois il est invoqué ou sur le nombre de threads il est invoqué.

12voto

Derek Park Points 25025

En gros, vous demandez pour la synchronisation de la création d'un singleton, sans utiliser de synchronisation (déjà construit des variables). En général, non, ce n'est pas possible. Vous avez besoin de quelque chose de disponible pour la synchronisation.

Comme pour ton autre question, oui, les variables statiques qui peuvent être initialisé de manière statique (pas d'exécution de code nécessaire) sont garantis d'être initialisé avant que les autres le code est exécuté. Cela rend possible l'utilisation d'une manière statique initialisé mutex pour synchroniser la création du singleton.

À partir de 2003 la révision de la norme C++:

Les objets statiques durée de stockage (3.7.1) doit être initialisé à zéro (8.5) avant toute autre initialisation a lieu. Zéro-initialisation et l'initialisation avec une expression constante sont collectivement appelés initialisation statique; tous les autres initialisation dynamique de l'initialisation. Les objets de types POD (3.9) avec la durée de stockage statique initialisé avec des expressions constantes (5.19) doit être initialisé avant tout dynamique de l'initialisation a lieu. Les objets statiques de stockage durée définie dans l'espace de noms de champ dans la même unité de traduction et dynamique initialisé doit être initialisé dans l'ordre de leur définition s'affiche dans l'unité de traduction.

Si vous savez que vous allez être en utilisant ce singleton lors de l'initialisation d'autres objets statiques, je pense que vous trouverez que la synchronisation est un non-problème. Au meilleur de ma connaissance, tous les principaux compilateurs initialiser les objets statiques dans un seul thread, threads de sécurité pendant l'initialisation statique. Vous pouvez déclarer votre singleton pointeur à NULL, et ensuite de vérifier pour voir si elle a été initialisée avant de l'utiliser.

Toutefois, cela suppose que vous savez que vous allez utiliser ce singleton pendant l'initialisation statique. Cela n'est pas garanti par la norme, donc si vous voulez être totalement en sécurité, utilisez une manière statique initialisé mutex.

Edit: Chris suggestion pour l'utilisation atomique compare-and-swap serait certainement. Si la portabilité n'est pas un problème (et la création d'temporaires supplémentaires singletons n'est pas un problème), alors il est légèrement inférieure à la surcharge de la solution.

10voto

Frerich Raabe Points 23711

Voici une très simple paresseusement construit singleton de lecture:

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

C'est paresseux, et la prochaine norme C++ (C++0x) nécessite, pour être thread-safe. En fait, je crois qu'au moins g++ implémente cette dans un thread de manière sûre. Donc si c'est votre cible compilateur ou si vous utilisez un compilateur qui implémente également présent dans un thread de manière sûre (peut-être plus récente de Visual Studio compilateurs faire? Je ne sais pas), alors ce pourrait être tout ce dont vous avez besoin.

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

9voto

Chris Jester-Young Points 102876

Vous ne pouvez pas le faire sans aucune variable statique, mais si vous êtes prêt à en tolérer une, vous pouvez utiliser Boost.Thread à cet effet. 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 renvoyez-le.

6voto

0124816 Points 804

Pour gcc, c'est plutôt facile:

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

GCC sera assurez-vous 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 à une nouvelle entre les tests, ou vous voulez changer la LazyType* pour un MockLazyType*, vous ne serez pas en mesure de. Compte tenu de cela, il est généralement préférable d'utiliser un statique mutex + pointeur statique.

Aussi, éventuellement, de son côté: Il est préférable de toujours éviter les non-POD types. (Pointeurs vers les Gousses sont OK.) Les raisons à cela sont nombreuses: comme vous le mentionnez, l'ordre d'initialisation n'est pas défini ni est l'ordre dans lequel les destructeurs sont appelés, si. De ce fait, les programmes s'écraser quand ils essaient de sortie; le plus souvent, pas une grosse affaire, mais parfois un obstacle de taille lorsque le générateur de profils que vous essayez d'utiliser nécessite un nettoyage de sortie.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X