Il y a des modes en programmation, des “il faut” et des “il ne faut”, impératifs, sans discussion possible alors qu’ils sortent du néant pour y retourner tôt ou tard… Le Double Check sur un Lock est-il de ce type ?
Le Double Check
Le double contrôle. “C’est bien, la preuve c’est que même Google insiste sur la double authentification”. Je l’ai entendu. Sans rire.
Deux petites choses : d’une part cela n’a rien à voir, ce qui montre à quel point beaucoup d’informaticiens auraient mieux fait d’être charcutiers ou chauffeur-livreurs, de nobles métiers utiles qui demandent malgré tout des capacités intellectuelles moindres. D’autre part cela démontre parfaitement le snobisme qui peut régner dans notre métier où les comportements sont souvent plus ceux de midinettes fashion-victims que d’ingénieurs sérieux et réfléchis.
Bref, le double check c’est de la frime ou bien ? (à dire avec l’accent suisse).
Commençons par le début.
Le double contrôle consiste à contrôler deux fois une même variable, avant le Lock et à l’intérieur de celui-ci au cas où un petit lutin malicieux aurait glisser un thread sournois entre le premier test et l’acquisition du Lock. Dans les faits cela peut arriver, le monde du multithreading est plein de petits lutins malicieux dont il faut déjouer les plans diaboliques.
On se sert le plus souvent d’un double check lors de la création d’un singleton. On peut avoir à s’en servir dans d’autres cas mais celui qu’on rencontre le plus souvent c’est celui du singleton.
Voici un exemple typique de double check :
public sealed class UsefulClass
{
private static object _synchBlock = new object();
private static volatile UsefulClass_singletonInstance;
//Pas de création depuis l'extérieur de la classe
private UsefulClass() {}
//Singleton property.
public static UsefulClassSingleton
{
get
{
if(_singletonInstance == null)
{
lock(_synchBlock)
{
// Si un lutin malicieux glisse un autre thread entre le 1er
// test et l'acquisition du lock.. donc on double check ici :
if(_singletonInstance == null)
{
_singletonInstance = new UsefulClass();
}
}
}
}
}
}
- A quoi sert tout ce code ?
- A créer un singleton.
- Mais pourquoi tout ce code ?
- Hmm pour permettre une instanciation “lazy” du singleton.
- Oui mais pourquoi ?
- Heuu tu m’énerves avec tes questions là !
C’est un peu ça le problème… C’est qu’en réalité cela s’appelle de la micro-optimisation ou de l’optimisation prématurée.
Que cherche-t-on en réalité ? A créer un singleton mais uniquement lors de son premier accès. La raison est que cela évite de créer l’instance si elle n’est pas utilisée.
Ca tient debout à votre avis ?
On pourrait se dire que si le singleton n’est pas utilisé par l’application on voit mal à quoi ça sert de le coder … Mais admettons qu’il existe des cas où cela puisse se justifier.
Du coup pour éviter la création d’une instance (celle du singleton) on créée systématiquement une instance de object pour le Lock… Et on ajoute plein de code pour tester tous les cas de figure les plus vicieux.
Est-ce bien raisonnable ? Peut-être, mais il faut que la création de l’instance du singleton soit sacrément couteuse pour justifier à la fois le code de test et la création permanente d’une instance de object.
C’est là qu’on mesure le ridicule de la situation. Il n’est pas ridicule de faire des singleton, il n’est pas ridicule de vouloir les charger avec un peu de délai, il n’est pas ridicule de double contrôler la variable, non, rien de tout cela est ridicule “par essence”. Chacune de ces choses peut se justifier dans des contextes données. Ce qui est ridicule c’est que dans 99% des cas cela ne sert à rien.
C’est de la micro optimisation, du vent, de la perte de temps.
La version sans contrôle du tout
S’il s’agit de créer un singleton et une fois qu’on a compris que le créer plus tard n’a que rarement du sens il existe un code beaucoup plus plus simple et plus fiable aussi :
public sealed class UsefulClass
{
private static UsefulClass _singletonInstance = new UsefulClass();
//pas de création en dehors de UsefulClass.
private UsefulClass() {}
//Accès au singleton.
public static UsefulClass Singleton =>_singletonInstance;
}
}
}
Nettement plus court… Et plus fiable aussi car c’est ici .NET qui nous garantit que le code d’initialisation d’une classe statique n’est exécuté qu’une seule fois. Cela est vrai tout le temps même en multithreading. D’ailleurs c’est encore mieux que cela, c’est le CLR plus que .NET qui le garantit.
Il n’y a donc aucune raison de faire des double contrôles alors que le Framework peut gérer la situation encore mieux tout seul !
Et en mode lazy ?
Il se peut, mais c’est assez rare, que cela vaille réellement la peine de différer la création du singleton. Mais même là le Framework nous fournit des outils qui évitent d’avoir à utiliser le double check :
public sealed class Singleton
{
/// la Lambda sera exécutée au premier appel
private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
/// l'instance en mode lazy
public static Singleton Instance => lazy.Value;
/// pas de création hors de la classe
private Singleton() { }
}
En utilisant la classe Lazy avec une expression lambda qui ne sera exécutée qu’au premier appel on s’appuie sur des mécanismes existants et fiables.
Le double check clap de fin ?
Je le disais le double check ne sert pas qu’aux singletons et on peut utiliser ce pattern dans d’autres occasions où cela peut avoir un intérêt.
D’ailleurs revenons un instant sur le double check lui-même. Car il n’est pas inutile, au contraire, au moins de le comprendre !
Ce pattern permet de s’assurer de plusieurs choses :
- Un seul thread peut entrer dans la section critique (grâce au Lock);
- Une seule instance est créée et uniquement une et quand cela est nécessaire;
- La variable est déclarée volatile pour s’assurer que l’assignation de la variable d’instance sera complète avant tout autre accès en lecture;
- On utilise une variable de verrouillage plutôt que de locker le type lui-même pour éviter les dead-locks;
- Le double check lui-même de la variable permet de résoudre le problème de la concurrence tout en évitant d’avoir à poser un Lock à chaque lecture de la propriété d’accès à l’instance.
Le premier check évite de placer un lock à chaque lecture de la propriété d’instance ce qui fonctionne correctement puisque la variable est volatile. Le second check s’assure que la variable n’est instanciée qu’une seule fois. Mais pourquoi ? Les histoires de lutins malicieux c’est amusant mais peu technique, alors voici un scénario qui planterait le singleton s’il n’y avait pas de double check :
- Thread 1 acquiert le lock
- Thread 1 commence l’initialisation de l’objet singleton
- Thread 2 entre et se trouve bloqué par le lock, il attend
- Thread 1 termine l’initialisation et sort
- Thread 2 peut enfin entrer dans la section lock et débuter une nouvelle initialisation !
Comme on le voit le pattern lui-même est loin d’être inutile. Il est même crucial dans certains contexte pour éviter à la fois les dead-locks et les doubles entrées dans des sections critiques (lorsqu’elle dépendent d’une variable, c’est pourquoi le cas du singleton est celui où cela est le plus utilisé puisqu’il s’agit déjà d’un pattern visant à contrôler la création d’une instance via une variable ou propriété). On notera que la variable instance doit être volatile pour garantir le fonctionnement ici décrit.
Donc pas de clap de fin pour le pattern mais peut-être pour le singleton…
En effet, le Singleton c’est l’antithèse de la POO. Avant la POO on avait un fichier avec tout un tas de fonctions dedans. Avec le Singleton on a une classe avec tout un tas de méthodes dedans. C’est un peu la POO pour débutant. Pas besoin de savoir combien d’instances il faudra gérer, il n’y en aura qu’une, tout redevient linéaire et simple à comprendre, comme à l’ancien temps quoi. C’était mieux avant. Un grand classique !
Certes le Singleton est un pattern décrit par le Gang Of Four, donc c’est forcément bien… Pas sûr. Les exemples donnés pour justifier le Singleton sont de type driver d’impression. Or ce n’est pas tout à fait le type de programme qu’on écrit tous les jours… Les Singleton créée des zones d’étranglement dans un environnement multithread, ce qu’impose toute programmation moderne en raison de l’évolution des processeurs. Du Temps du GOF les processeurs étaient mono-cœurs et l’évolution se faisait par la monté en Ghz. Depuis au moins 20 ans la progression ne se fait plus de cette façon mais par la généralisation des multi-cœurs. Un bon développement utilise forcément de nos jours du multithreading. Dans ce contexte très différent de l’époque du GOF le Singleton devient un problème plus qu’une solution.
En général ils servent plus à rendre des services qu’à gérer une device (exemple du driver donné par le GOF). Toutefois pour rendre des services nous avons des constructions mieux adaptées qui n’imposent pas d’avoir de singletons et qui utilisent l’injection de dépendances principalement, la messagerie MVVM ou un service locator. On notera que ce dernier peut être un singleton car il ne joue qu’un rôle d’aiguillage et ne constitue pas une zone de blocage, en revanche le code qui est derrière se doit d’être thread safe.
Donc oui au double-check, bien entendu, là où cela est indispensable, mais pour le singleton… avant d’en créer … double-checkez votre réflexion avant de le faire !
Conclusion
Le double-ckeck locking possède de solides justifications techniques dans un environnement multitreadé. Il ne s’agit ni d’une mode ni de frime, c’est utile.
Mais c’est utile uniquement dans certains cas très limités. Notamment pour la création des singletons.
Or il existe d’autres façons de créer un singleton encore plus fiable et plus courtes…
Et il subsiste une question : “les singletons programmation à papa ou vraie utilité ?”
A vous d’y réfléchir, à deux fois !
Stay Tuned !