OVH Cloud OVH Cloud

[HS] Design Patterns : Singleton

20 réponses
Avatar
Stephane Wirtel
Bonsoir,

J'aurais aimé avoir votre avis sur la question concernant
l'implémentation du Design Pattern "Singleton" dans le cas d'une
application multi-thread.

J'ai pû constater que Mr Douglas C. Schmidt proposait comme solution, le
fait d'employer le Double Checked Locking, mais en faisant une recherche
sur google, j'ai pû constater que des exemples donnaient des résultats
contradictoires concernant ce pattern.

Auriez-vous un conseil à me fournir afin d'implémenter une classe
Singleton qui puissent fonctionner dans un environnement multi-thread ?

Merci,

Stéphane WIRTEL.

10 réponses

1 2
Avatar
Jean-Marc Bourguet
Laurent Deniau writes:

wrote:
Jean-Marc Bourguet wrote:

Laurent Deniau writes:

Jean-Marc Bourguet wrote:

Laurent Deniau writes:

Si c'est ce que tu cherches, je ne vois pas l'interet du test avec NULL


La possibilite d'utiliser le singleton dans
l'initialisation de variables statiques (donc a un moment
ou ourInstance n'a potentiellement pas encore ete
initialisee).


Ok. On peut reformuler:

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

private:
Singleton() {}
} ;

Singleton& Singleton::instance()
{
static Singleton& myInstance = *new Singleton;
return myInstance;
}


C'est ce que j'utilise (presque, je n'aime pas l'utilisation
de la reference, j'utilise donc un pointeur) J'utilise meme

Singleton& Singleton::instance()
{
static Singleton myInstance;
return myInstance;
}

quand je n'ai pas a choisir une classe derivee. Je ne sais pas
pourquoi James fait autre chose.
Deux raisons. La raison pour le new est simple : je peux aussi

utiliser le singleton dans des destructeurs des statiques, sans
risque qu'il soit déjà detruit. La raison pour l'initialisation
explicite en dehors de la fonction, c'est simplement pour
assurer que instance est appelé au moins une fois pendant les
initialisations static, avant le démarrage des threads. Et que
donc, il n'a pas besoin de lock. J'aurais pu utiliser n'importe
quelle variable statique, mais je le trouve assez succinct (mais
peut-être pas trop lisible) d'utiliser précisement le pointeur
d'instance du singleton.


Mais est-ce que:

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

private:
Singleton() {}
static Singleton myInstance;
} ;

Singleton Singleton::myInstance;
Singleton& Singleton::instance() { return myInstance; }

ne rempli pas le cahier des charges?


James veut

- pouvoir utiliser le singleton dans les constructeurs
d'objets statiques: donc l'objet doit être ou une
variable statique dans une fonction (qui en retourne
une référence) ou une variable globale pointeur (on utilise
l'initialisation à NULL faite statiquement à la
compilation ou au chargement pour voir si elle a déjà
été initialisée)

- être sur que l'initialisation soit faite avant l'entrée
dans main (ce qui exclus la variable statique dans la
fonction qui ne serait pas initialisée)

- être utilisable dans les destructeurs d'objets
statiques (ce qui exclus toute méthode où le singleton
est détruit).

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org






Avatar
Arnaud Debaene
wrote:


Le DCLP ne marche à priori que sur des processeurs ayant une
cohérence forte de cache (X86...). Si tu veux être générique
et portable, utilises un lock pour chaque appel à
"GetInstance".


Le DCLP ne marche a priori pas. Un point, c'est tout. Tel qu'il
est implémenté dans ACE, il ne marche même pas sur le 80x86 le
plus primitif, avec un seul processeur. S'il donne l'air de
marcher, parfois, c'est parce que le compilateur n'optimise pas.


Ok, alors soyons plus précis : Si les processeurs ont une force cohérence de
cache et si je mets le code d'initialisation dans une autre unité de
translation que le code du singleton (l'initialisation se fait par un appel
de fonction - et je considère que le compilateur ne fait pas d'optimisation
globale, ou alors je l'interdis), est-e qu'il y a quelque chose qui te
chagrine avec le DCLP?

Et si tu peux nous donner un exemple (ou un lien) sur un problème possible
avec un *monoprocesseur* (quel qu'il soit), je suis preneur parce que là je
ne vois vraiement pas!

Arnaud


Avatar
Patrick Mézard
Arnaud Debaene wrote:
Ok, alors soyons plus précis : Si les processeurs ont une force cohérence de
cache et si je mets le code d'initialisation dans une autre unité de
translation que le code du singleton (l'initialisation se fait par un appel
de fonction - et je considère que le compilateur ne fait pas d'optimisation
globale, ou alors je l'interdis), est-e qu'il y a quelque chose qui te
chagrine avec le DCLP?


Par curiosité, qu'est-ce tu veux dire précisément par "les processeurs
ont une forte cohérence de cache" ? Tu as un exemple de ce genre
d'architecture dans la vie courante ?

Patrick Mézard

Avatar
Arnaud Debaene
Patrick Mézard wrote:
Arnaud Debaene wrote:
Ok, alors soyons plus précis : Si les processeurs ont une force
cohérence de cache et si je mets le code d'initialisation dans une
autre unité de translation que le code du singleton
(l'initialisation se fait par un appel de fonction - et je considère
que le compilateur ne fait pas d'optimisation globale, ou alors je
l'interdis), est-e qu'il y a quelque chose qui te chagrine avec le
DCLP?


Par curiosité, qu'est-ce tu veux dire précisément par "les processeurs
ont une forte cohérence de cache" ?
Un système où, lorsqu'un processeur effectue une opération d'écriture, une

interruption prioritaire interrompts immédiatement tous les autres
processeurs pour qu'ils marquent l'adresse correspondante comme "dirty" dans
leur cache local, et évitent donc de lire une version "ancienne" cachée
localement de la valeur.Un mécanisme similaire a lieu également lors de la
lecture d'une variable.
Ce système simplifie la programmation multi-tâches car il n'y a pas besoin
de "barrière" applicative pour mettre à jour les caches des processeurs,
mais elle est très mauvaise en terme de "scalability" (traduction
française????) car, avec beaucoup de processeurs, elle entraine une trafic
pas possible entre processeurs (souvent sur un bus dédié à ces mises-à-jour
de cache) et un taux très important d'interruptions. La tendance actuelle
est de laisser tomber ce système à cause de ces limitations.

Tu as un exemple de ce genre
d'architecture dans la vie courante ?
Les 80X86 jusqu'à présent.


Arnaud
MVP - VC


Avatar
Patrick Mézard
Arnaud Debaene wrote:
Patrick Mézard wrote:

Arnaud Debaene wrote:

Ok, alors soyons plus précis : Si les processeurs ont une force
cohérence de cache et si je mets le code d'initialisation dans une
autre unité de translation que le code du singleton
(l'initialisation se fait par un appel de fonction - et je considère
que le compilateur ne fait pas d'optimisation globale, ou alors je
l'interdis), est-e qu'il y a quelque chose qui te chagrine avec le
DCLP?




Il faut sans doute ajouter que les opérations de lecture/écriture de
pointeurs correctement alignés sont atomiques (guaranti par Intel pour
les IA-32). Dans le cas contraire, il peut y avoir du "byte-tearing" sur
le pointeur de l'instance du singleton.

Moyennant ça, ça doit marcher. Mais ce sont tout de même des contraintes
assez fortes, et dans le cas de IA-32 pas garanties dans le futur (cf
IA-32 Intel Architecture Software Developer's Manual, III-7.2.4, au
sujet du modèle de cohérence de cache). Et je ne parle pas de ton
interdiction des optimisations globales au niveau du compilateur/linker.

Les caches sont généralement considérés comme transparents au niveau des
processeurs, ce sont les règles de visibilité mémoire (et donc plutôt
les possibilités de réordonnancement des lectures/écritures par le
contrôleur de mémoire) qui posent problème.


Par curiosité, qu'est-ce tu veux dire précisément par "les processeurs
ont une forte cohérence de cache" ?


Un système où, lorsqu'un processeur effectue une opération d'écriture, une
interruption prioritaire interrompts immédiatement tous les autres
processeurs pour qu'ils marquent l'adresse correspondante comme "dirty" dans
leur cache local, et évitent donc de lire une version "ancienne" cachée
localement de la valeur.Un mécanisme similaire a lieu également lors de la
lecture d'une variable.
Ce système simplifie la programmation multi-tâches car il n'y a pas besoin
de "barrière" applicative pour mettre à jour les caches des processeurs,
mais elle est très mauvaise en terme de "scalability" (traduction
française????) car, avec beaucoup de processeurs, elle entraine une trafic
pas possible entre processeurs (souvent sur un bus dédié à ces mises-à-jour
de cache) et un taux très important d'interruptions. La tendance actuelle
est de laisser tomber ce système à cause de ces limitations.


Tu as un exemple de ce genre
d'architecture dans la vie courante ?


Les 80X86 jusqu'à présent.


[Ce n'était pas une question piège :-), je viens juste de découvrir ça.
L'architecture décrite paraissait quand même très contraignante pour des
processeurs "modernes"]

Concernant la partie lecture d'une variable, c'était vrai jusqu'au
Pentium compris. Les suivants autorisent certaines opérations de
lectures à migrer devant d'autres opérations d'écritures. En revanche,
l'ordre des opérations d'écritures d'un processeur est le même vu des
autres processeurs [IA-32 Intel Architecture Software Developer's
Manual, III-7.2.2]. C'est ça qui permet éventuellement au DCL de marcher
sur IA-32. Ceci dit (et même si c'est improbable) :

"""
Despite the fact that Pentium 4, Intel Xeon, and P6 family processors
support processor ordering, Intel does not guarantee that future
processors will support this model. To make software portable to future
processors, it is recommended that operating systems provide critical
region and resource control constructs and API’s (application program
interfaces) based on I/O, locking, and/or serializing instructions be
used to synchronize access to shared areas of memory in
multiple-processor systems. Also, software should not depend on
processor ordering in situations where the system hardware does not
support this memory-ordering model.
"""

Barrière applicative => barrière memoire ?
Scalability => dimensionnement ?

Patrick Mézard



Avatar
kanze
Laurent Deniau wrote:
wrote:
Jean-Marc Bourguet wrote:

C'est ce que j'utilise (presque, je n'aime pas l'utilisation
de la reference, j'utilise donc un pointeur) J'utilise meme

Singleton& Singleton::instance()
{
static Singleton myInstance;
return myInstance;
}

quand je n'ai pas a choisir une classe derivee. Je ne sais
pas pourquoi James fait autre chose.


Deux raisons. La raison pour le new est simple : je peux
aussi utiliser le singleton dans des destructeurs des
statiques, sans risque qu'il soit déjà detruit. La raison
pour l'initialisation explicite en dehors de la fonction,
c'est simplement pour assurer que instance est appelé au
moins une fois pendant les initialisations static, avant le
démarrage des threads. Et que donc, il n'a pas besoin de
lock. J'aurais pu utiliser n'importe quelle variable
statique, mais je le trouve assez succinct (mais peut-être
pas trop lisible) d'utiliser précisement le pointeur
d'instance du singleton.


Mais est-ce que:

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

private:
Singleton() {}
static Singleton myInstance;
} ;

Singleton Singleton::myInstance;
Singleton& Singleton::instance() { return myInstance; }

ne rempli pas le cahier des charges?


Pas du tout. Si on appelle Singleton::instance() du constructeur
d'un objet statique dans une autre unité de compilation, je
risque de me rétrouvait avec un objet non-encore construit. Et
si je l'utilise dans le destructeur d'un tel objet, je risque de
me rétrouvait avec un objet déjà detruit.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
kanze
Arnaud Debaene wrote:
wrote:

Le DCLP ne marche à priori que sur des processeurs ayant
une cohérence forte de cache (X86...). Si tu veux être
générique et portable, utilises un lock pour chaque appel à
"GetInstance".


Le DCLP ne marche a priori pas. Un point, c'est tout. Tel
qu'il est implémenté dans ACE, il ne marche même pas sur le
80x86 le plus primitif, avec un seul processeur. S'il donne
l'air de marcher, parfois, c'est parce que le compilateur
n'optimise pas.


Ok, alors soyons plus précis : Si les processeurs ont une
force cohérence de cache et si je mets le code
d'initialisation dans une autre unité de translation que le
code du singleton (l'initialisation se fait par un appel de
fonction - et je considère que le compilateur ne fait pas
d'optimisation globale, ou alors je l'interdis), est-e qu'il y
a quelque chose qui te chagrine avec le DCLP?


Voyons. Disons que si par hazard il marche, il marche. Et le
langage et tous les compilateurs que je connais reclament le
droit de déplacer le code à volenté, dans la mesure où le
comportement visible (qui ne prend pas en compte les threads) ne
chang pas. J'ai déjà vu les compilateurs qui le faisait. De chez
Microsoft -- alors, pas les exotiques.

En mettant le constructeur dans une autre unité de compilation,
tu augmentes tes chances, mais tu n'as toujours pas de garantie.
Pire, qu'est-ce qui te garantit que suite à un problème de
performance ailleurs, un programmeur de maintenance ne rend pas
le constructeur inline ? Il faut donc une documentation spéciale
disant que d'une part, il ne faut jamais mettre le constructeur
inline, ni dans la même unité de compilation que la fonction
instance, et de l'autre, que le code ne marche qu'avec un
compilateur bien précis, telle et telle version, avec de telles
options de compilation, et que lors du moindre changement, il
faudrait révérifier en regardant de nouveau le code généré
(parce que ce genre de chose, tu ne peux pas réelement le
tester).

Et si tu peux nous donner un exemple (ou un lien) sur un
problème possible avec un *monoprocesseur* (quel qu'il soit),
je suis preneur parce que là je ne vois vraiement pas!


Le problème avec les monoprocesseurs, c'est dû au compilateur.
Un autre problème avec les monoprocesseurs, c'est qu'ils
devienent multiprocesseurs lors d'une mise à jour du hardware,
sans qu'on te previent.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
kanze
Arnaud Debaene wrote:
Patrick Mézard wrote:
Arnaud Debaene wrote:
Ok, alors soyons plus précis : Si les processeurs ont une
force cohérence de cache et si je mets le code
d'initialisation dans une autre unité de translation que le
code du singleton (l'initialisation se fait par un appel de
fonction - et je considère que le compilateur ne fait pas
d'optimisation globale, ou alors je l'interdis), est-e
qu'il y a quelque chose qui te chagrine avec le DCLP?


Par curiosité, qu'est-ce tu veux dire précisément par "les
processeurs ont une forte cohérence de cache" ?


Un système où, lorsqu'un processeur effectue une opération
d'écriture, une interruption prioritaire interrompts
immédiatement tous les autres processeurs pour qu'ils marquent
l'adresse correspondante comme "dirty" dans leur cache local,
et évitent donc de lire une version "ancienne" cachée
localement de la valeur.Un mécanisme similaire a lieu
également lors de la lecture d'une variable.


Mais ça, c'est vrai pour prèsque tous les processeurs (sauf que
le mot interrompre prête à confusion -- il n'y a pas
d'interruption au sens classique). Mais ça ne dit rien. La cache
n'y est pour rien.

Ce qu'il faut pour que l'algorithme ait des chances de marcher,
c'est que les écritures apparaissent à tous les autres
processeurs dans l'ordre qu'on les a écrit. Or, déjà, le
compilateur a le droit de les réordonner -- et beaucoup de
compilateurs le font dans certains cas. Ensuite, beaucoup de
processeurs peuvent réordonner l'ordre des accès mémoire ; si
j'ai bien compris, même un Intel 32 bits le fait dans certains
cas limités.

Ce système simplifie la programmation multi-tâches car il n'y
a pas besoin de "barrière" applicative pour mettre à jour les
caches des processeurs, mais elle est très mauvaise en terme
de "scalability" (traduction française????) car, avec beaucoup
de processeurs, elle entraine une trafic pas possible entre
processeurs (souvent sur un bus dédié à ces mises-à-jour de
cache) et un taux très important d'interruptions. La tendance
actuelle est de laisser tomber ce système à cause de ces
limitations.


Le problème ne concerne pas la cache, qui est, autant que je
sache, toujours plus ou moins synchronisé. Le problème concerne
le fonctionnement du pipeline d'accès mémoire dans le
processeur.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
Laurent Deniau
wrote:
Laurent Deniau wrote:

wrote:

Jean-Marc Bourguet wrote:




C'est ce que j'utilise (presque, je n'aime pas l'utilisation
de la reference, j'utilise donc un pointeur) J'utilise meme





Singleton& Singleton::instance()
{
static Singleton myInstance;
return myInstance;
}





quand je n'ai pas a choisir une classe derivee. Je ne sais
pas pourquoi James fait autre chose.





Deux raisons. La raison pour le new est simple : je peux
aussi utiliser le singleton dans des destructeurs des
statiques, sans risque qu'il soit déjà detruit. La raison
pour l'initialisation explicite en dehors de la fonction,
c'est simplement pour assurer que instance est appelé au
moins une fois pendant les initialisations static, avant le
démarrage des threads. Et que donc, il n'a pas besoin de
lock. J'aurais pu utiliser n'importe quelle variable
statique, mais je le trouve assez succinct (mais peut-être
pas trop lisible) d'utiliser précisement le pointeur
d'instance du singleton.




Mais est-ce que:



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

private:
Singleton() {}
static Singleton myInstance;
} ;



Singleton Singleton::myInstance;
Singleton& Singleton::instance() { return myInstance; }



ne rempli pas le cahier des charges?



Pas du tout. Si on appelle Singleton::instance() du constructeur
d'un objet statique dans une autre unité de compilation, je
risque de me rétrouvait avec un objet non-encore construit. Et


Ok. J'avais garder en tete une idee fausse comme quoi une reference est
toujours initialisee avant son utilisation. instance() retournant une
reference, je pensais que ca garantissait l'initialisation de
myInstance. Mais j'ai regarde dans le draft et j'ai rien trouve de tel.
Autant pour moi.

si je l'utilise dans le destructeur d'un tel objet, je risque de
me rétrouvait avec un objet déjà detruit.


Ca Ok.

a+, ld.




Avatar
Jean-Marc Bourguet
writes:

Le problème ne concerne pas la cache, qui est, autant que je
sache, toujours plus ou moins synchronisé.


A ma connaissance, les moins synchronisés ne permettent pas
le DCL.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

1 2