Double-checked locking

Double-checked locking

En génie logiciel, le verrouillage à double test ou double-checked locking est un ancien patron de conception[1].

Considéré aujourd'hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu'il pose, il a été utilisé dans le passé pour réduire le surcoût d'acquisition du verrou nécessaire à l'initialisation tardive d'un singleton, en commençant par vérifier l'objet du verrou sans précaution avant, ensuite, de poser effectivement le verrou.

Sommaire

En Java

Prenons par exemple la classe Java suivante :

// Version mono-thread
class Singleton1 {
    private Outil outil = null;
    public Outil getOutil() {
        if (outil == null) {
            outil = new Outil();
        }
        return outil;
    }
 
    // autres attributs et méthodes...
}

Le problème de cette classe est qu'elle ne fonctionne pas en environnement multi-thread. Si deux threads appellent pour la première fois getOutil() en même temps, chacun va créer l'objet en même temps que l'autre, ou l'un va obtenir une référence sur un objet partiellement initialisé. Pour éviter cela, il faut utiliser un verrou, ce qui se fait grâce à la synchronisation, comme le montre l'exemple de code ci-dessous.

// Version correcte en environnement multi-thread, mais potentiellement coûteuse
class Singleton2 { 
    private Outil outil = null;
    public synchronized Outil getOutil() {
        if (outil == null) {
            outil = new Outil();
        }
        return outil;
    }
 
    // autres attributs et méthodes...
}

Ici, le premier appel à getOutil() va créer l'objet, et si d'autres threads appellent getOutil() pendant que l'objet est cours d'initialisation, ils seront mis en attente par la synchronisation jusqu'à ce que l'objet soit complètement initialisé et sa référence retournée par le premier appel du premier thread. Tous les appels suivants ne feront que retourner la référence vers l'objet précédemment initialisé.

Cependant, la synchronisation d'une méthode peut augmenter considérablement le temps de son exécution[2]. L'acquisition et la libération d'un verrou à chaque fois que la méthode est appelée pourrait donc sembler un surcoût inutile. De nombreux programmeurs ont essayé d'optimiser ce code comme suit :

  1. Vérifier que la variable n'est pas encore initialisée (sans tenter d'acquérir un verrou). Si elle l'est, retourner sa valeur immédiatement.
  2. Acquérir un verrou.
  3. Re-vérifier que la variable n'est pas encore initialisée : si un autre thread a acquis le verrou juste avant, il pourrait avoir initialisé la variable entre temps. S'il s'avère que la variable est initialisée, retourner sa valeur.
  4. Sinon, initialiser la variable et retourner sa valeur.
// Version multithreaded erronée
// Motif "Double-Checked Locking"
class Singleton3 {
    private Outil outil = null;
    public Outil getOutil() {
        if (outil == null) {
            synchronized (this) {
                if (outil == null) {
                    outil = new Outil();
                }
            }
        }
        return outil;
    }
 
    // autres attributs et méthodes...
}

Intuitivement, cet algorithme semble une solution efficace au problème posé. Cependant, il soulève des problèmes subtils et difficiles à tracer. Ainsi, considérons la séquence d'événements suivante :

  1. Le thread A remarque que la variable partagée n'est pas initialisée. Il acquiert donc le verrou et commence à initialiser la variable.
  2. Du fait de la sémantique du langage de programmation, le code généré par le compilateur a le droit de modifier la variable partagée avant que A ait terminé l'initialisation, de sorte que la variable référence un objet partiellement initialisé (par exemple parce que le thread commence par allouer la mémoire, puis place la référence vers le bloc mémoire alloué dans la variable, avant, enfin, d'appeler le constructeur qui va initialiser le bloc mémoire).
  3. Le thread B remarque que la variable partagée a été initialisée (ou, tout au moins, semble l'être), et retourne sa valeur immédiatement sans tenter d'acquérir un verrou. Si, ensuite, la variable partagée est utilisée avant que A n'ait eu le temps de terminer son initialisation, le programme risque fort de planter.

L'un des dangers du double-checked locking est que, souvent, cela semblera fonctionner. Cela dépend du compilateur, de la manière dont les threads sont ordonnancés par le système d'exploitation, et aussi d'autres mécanismes de gestion de la concurrence d'accès aux données. Reproduire les cas de plantage peut s'avérer d'autant plus difficile qu'ils sont hautement improbables lorsque le code est exécuté au sein d'un débogueur. L'utilisation du double-checked locking doit donc être bannie autant que possible.

Néanmoins, JavaSE 5.0 propose une solution à ce problème avec le mot réservé volatile qui garantit que des threads différents gèrent correctement l'accès concurrent à l'instance unique du singleton[3]:

// Fonctionne grâce à le nouvelle sémantique du mot-clé volatile
// Ne fonctionne pas avec Java 1.4 ou précédent
class Singleton4 {
    private volatile Outil outil = null;
    public Outil getOutil() {
        if (outil == null) {
            synchronized (this) {
                if (outil == null) {
                    outil = new Outil();
                }
            }
        }
        return outil;
    }
 
    // autres attributs et méthodes...
}

Cependant, cette sécurité d'accès a un prix : l'accès à une variable volatile est moins efficace que l'accès à une variable normale.

De nombreuses versions du patron double-checked locking qui n'utilisent pas de synchronisation explicite ou de variable volatile ont été proposées. A part celles qui reposent sur le mot-clé enum ou le motif de support d'initialisation à la demande, toutes se sont révélées incorrectes[4],[5].

Avec Microsoft Visual C++

On peut implémenter le double-checked locking avec Visual C++ 2005 si le pointeur vers la ressource est déclaré volatile. Visual C++ 2005 garantit que les variables volatiles se comportent comme des barrières, comme avec JavaSE 5.0, empêchant aussi bien le compilateur que le processeur de réordonnancer les lectures et écritures à ces variables pour les rendre plus efficaces.

Les versions précédentes de Visual C++ n'offraient pas cette garantie. Cependant, marquer le pointeur vers la ressource comme volatile pénalise les performances d'une manière ou d'une autre, en particulier si le pointeur est visible ailleurs dans le code : le compilateur le traitera comme une barrière partout où il est utilisé, même lorsque cela n'est pas nécessaire.

Voir aussi

Références

  1. Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. Boehm, Hans-J. "Threads Cannot Be Implemented As a Library", ACM 2005, p265
  3. La nouvelle sémantique du mot clé volatile est décrite dans [1]
  4. [2]
  5. [3]

Liens externes


Wikimedia Foundation. 2010.

Contenu soumis à la licence CC-BY-SA. Source : Article Double-checked locking de Wikipédia en français (auteurs)

Игры ⚽ Нужно решить контрольную?

Regardez d'autres dictionnaires:

  • Double checked locking — En génie logiciel, le verrouillage à double test ou double checked locking est un ancien patron de conception[1]. Considéré aujourd hui comme un antipattern du fait des problèmes subtils et difficiles à déceler qu il pose, il a été utilisé dans… …   Wikipédia en Français

  • Double-checked locking — In software engineering, double checked locking (also known as double checked locking optimization[1] .) is a software design pattern used to reduce the overhead of acquiring a lock by first testing the locking criterion (the lock hint ) without… …   Wikipedia

  • Double checked locking — Шаблон проектирования Блокировка с двойной проверкой Double checked locking Описан в Design Patterns Нет Double checked locking (блокировка с двойной проверкой) шаблон проектирования, применяющийся в параллельном программировании. Он… …   Википедия

  • Блокировка с двойной проверкой — Double checked locking (блокировка с двойной проверкой) шаблон проектирования применяющийся в параллельном программировании. Он предназначен для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие… …   Википедия

  • Anti-pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Antimuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Antipattern — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Negativmuster — Anti Pattern (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster,… …   Deutsch Wikipedia

  • Anti-Pattern — (deutsch: Antimuster) bezeichnet in der Softwareentwicklung einen häufig anzutreffenden schlechten Lösungsansatz für ein bestimmtes Problem. Es bildet damit das Gegenstück zu den Mustern (Entwurfsmuster, Analysemuster, Architekturmuster...),… …   Deutsch Wikipedia

  • Singleton pattern — In software engineering, the singleton pattern is a design pattern used to implement the mathematical concept of a singleton, by restricting the instantiation of a class to one object. This is useful when exactly one object is needed to… …   Wikipedia

Share the article and excerpts

Direct link
Do a right-click on the link above
and select “Copy Link”