3 votes

Ces barrières mémorielles sont-elles nécessaires ?

J'ai rencontré l'implémentation suivante de la fonction Singleton get_instance fonction :

template<typename T>
T* Singleton<T>::get_instance()
{
    static std::unique_ptr<T> destroyer;

    T* temp = s_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);

    if (temp == nullptr) 
    {
        std::lock_guard<std::mutex> lock(s_mutex);
        temp = s_instance.load(std::memory_order_relaxed);/* read current status of s_instance */
        if (temp == nullptr) 
        {
            temp = new T;

            destroyer.reset(temp);
            std::atomic_thread_fence(std::memory_order_release);
            s_instance.store(temp, std::memory_order_relaxed);
        }
    }

    return temp;
}

Et je me demandais si les barrières mémorielles d'acquisition et de libération présentaient un intérêt quelconque ? Pour autant que je sache, les barrières de mémoire sont destinées à empêcher le réordonnancement des opérations de mémoire entre deux variables différentes. Prenons l'exemple classique :

(Tout ceci est en pseudo-code - ne vous laissez pas surprendre par la syntaxe)

# Thread 1
while(f == 0);
print(x)

# Thread 2
x = 42;
f = 1;

Dans ce cas, nous voulons empêcher la réorganisation des deux opérations de stockage dans le Thread 2, et la réorganisation des deux opérations de chargement dans le Thread 1. Nous insérons donc des barrières :

# Thread 1
while(f == 0)
acquire_fence
print(x)

# Thread 2
x = 42;
release_fence
f = 1;

Mais dans le code ci-dessus, quel est l'avantage des clôtures ?

EDITAR

La principale différence entre ces deux cas est que, dans l'exemple classique, nous utilisons des barrières de mémoire puisque nous avons affaire à des 2 variables - nous avons donc le "danger" du stockage du fil 2 f avant le stockage x ou, alternativement, le danger de chargement dans le fil 1 x avant le chargement f .

Mais dans mon code Singleton, quelle est la réorganisation possible de la mémoire que les barrières de mémoire visent à empêcher ?

NOTA

Je sais qu'il y a d'autres moyens (et peut-être de meilleurs) d'y parvenir, ma question est à des fins éducatives - je me renseigne sur les barrières mémorielles et je suis curieux de savoir si, dans ce cas particulier, elles sont utiles. Je suis curieux de savoir si, dans ce cas particulier, elles sont utiles.

2voto

LWimsey Points 2916

La complexité de ce schéma (appelé double-checked-locking, ou DCLP) réside dans le fait que la synchronisation des données peut s'effectuer de deux manières différentes (en fonction des éléments suivants quand un lecteur accède au singleton) et elles se chevauchent en quelque sorte.
Mais puisque vous posez des questions sur les clôtures, passons la partie mutex.

Mais dans mon code Singleton, quelle est la réorganisation possible de la mémoire que les barrières de mémoire visent à empêcher ?

Cela n'est pas très différent de votre pseudo-code où vous avez déjà remarqué que les clôtures d'acquisition et de libération sont nécessaires pour garantir le résultat de 42.
f est utilisée comme variable de signalisation et x Il est préférable de ne pas le réorganiser.

Dans le modèle DCL, c'est le premier thread qui alloue la mémoire : temp = new T;
La mémoire temp va être accédée par d'autres threads, et doit donc être synchronisée (c'est-à-dire visible par d'autres threads).
La barrière de libération suivie par le magasin détendu garantit que le new est ordonnée avant le magasin de façon à ce que les autres threads respectent le même ordre. Ainsi, une fois que le pointeur est écrit dans le fichier atomique s_instance et d'autres threads lisent l'adresse à partir de s_instance ils auront également accès à la mémoire vers laquelle il pointe.

La clôture d'acquisition fait la même chose, mais dans l'ordre inverse ; elle garantit que tout ce qui est séquencé après le chargement détendu et la clôture (c'est-à-dire l'accès à la mémoire) ne sera pas visible par le thread qui a alloué cette mémoire. De cette manière, l'allocation de la mémoire dans un thread et son utilisation dans un autre ne se chevaucheront pas.

En autre réponse J'ai essayé de visualiser cela à l'aide d'un diagramme.

Notez que ces clôtures sont toujours associées, une clôture de libération sans acquisition n'a pas de sens, bien que vous puissiez également utiliser (et mélanger) des clôtures avec des opérations de libération/acquisition.

s_instance.store(temp, std::memory_order_release); // no standalone fence necessary

Le coût de DCLP est que chaque utilisation (dans chaque thread) implique un chargement/acquisition, qui nécessite au minimum un chargement non optimisé (c'est-à-dire un chargement à partir du cache L1). C'est pourquoi les objets statiques en C++11 (éventuellement implémentés avec DCLP) peuvent être plus lents qu'en C++98 (pas de modèle de mémoire).

Pour plus d'informations sur le DCLP, consultez cet article de Jeff Preshing.

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