web-dev-qa-db-fra.com

Comprendre les clôtures de mémoire C ++ 11

J'essaie de comprendre les clôtures de mémoire en c ++ 11, je sais qu'il existe de meilleures façons de le faire, des variables atomiques et ainsi de suite, mais je me suis demandé si cette utilisation était correcte. Je me rends compte que ce programme ne fait rien d'utile, je voulais juste m'assurer que l'utilisation des fonctions de clôture faisait ce que je pensais qu'elles faisaient.

Fondamentalement, la version garantit que toutes les modifications apportées à ce thread avant la clôture sont visibles pour les autres threads après la clôture, et que dans le deuxième thread que toutes les modifications apportées aux variables sont visibles dans le thread immédiatement après la clôture?

Ma compréhension est-elle correcte? Ou ai-je complètement raté le point?

#include <iostream>
#include <atomic>
#include <thread>

int a;

void func1()
{
    for(int i = 0; i < 1000000; ++i)
    {
        a = i;
        // Ensure that changes to a to this point are visible to other threads
        atomic_thread_fence(std::memory_order_release);
    }
}

void func2()
{
    for(int i = 0; i < 1000000; ++i)
    {
        // Ensure that this thread's view of a is up to date
        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a;
    }
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}
37
jcoder

Votre utilisation ne pas assure réellement les choses que vous mentionnez dans vos commentaires. Autrement dit, votre utilisation des clôtures ne garantit pas que vos affectations à a sont visibles pour les autres threads ou que la valeur que vous lisez à partir de a est "à jour". Cela est dû au fait que, même si vous semblez avoir une idée de base de l'endroit où les clôtures doivent être utilisées, votre code ne remplit pas réellement les conditions exactes pour que ces clôtures se "synchronisent".

Voici un exemple différent qui, je pense, illustre mieux l'utilisation correcte.

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<bool> flag(false);
int a;

void func1()
{
    a = 100;
    atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void func2()
{
    while(!flag.load(std::memory_order_relaxed))
        ;

    atomic_thread_fence(std::memory_order_acquire);
    std::cout << a << '\n'; // guaranteed to print 100
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}

La charge et le stockage sur l'indicateur atomique ne se synchronisent pas, car ils utilisent tous les deux l'ordre de la mémoire détendue. Sans les clôtures, ce code serait une course aux données, car nous effectuons des opérations conflictuelles avec un objet non atomique dans différents threads, et sans les clôtures et la synchronisation qu'elles fournissent, il n'y aurait pas de relation avant-entre les opérations conflictuelles sur a.

Cependant, avec les clôtures, nous obtenons la synchronisation car nous avons garanti que le thread 2 lira l'indicateur écrit par le thread 1 (car nous bouclons jusqu'à ce que nous voyions cette valeur), et puisque l'écriture atomique s'est produite après la clôture de la libération et la lecture atomique a lieu -avant la clôture d'acquisition, les clôtures se synchronisent. (voir § 29.8/2 pour les exigences spécifiques.)

Cette synchronisation signifie tout ce qui se produit - avant la clôture de la libération - avant tout ce qui se produit - après la clôture de l'acquisition. Par conséquent, l'écriture non atomique dans a se produit avant la lecture non atomique de a.

Les choses deviennent plus difficiles lorsque vous écrivez une variable dans une boucle, car vous pouvez établir une relation passe-avant pour une itération particulière, mais pas d'autres itérations, provoquant une course aux données.

std::atomic<int> f(0);
int a;

void func1()
{
    for (int i = 0; i<1000000; ++i) {
        a = i;
        atomic_thread_fence(std::memory_order_release);
        f.store(i, std::memory_order_relaxed);
    }
}

void func2()
{
    int prev_value = 0;
    while (prev_value < 1000000) {
        while (true) {
            int new_val = f.load(std::memory_order_relaxed);
            if (prev_val < new_val) {
                prev_val = new_val;
                break;
            }
        }

        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a << '\n';
    }
}

Ce code provoque toujours la synchronisation des clôtures mais n'élimine pas les courses de données. Par exemple, si f.load() arrive à renvoyer 10, alors nous savons que a=1, a=2, ... a=10 Se sont tous produits, avant ce cout<<a, Mais nous non savons que cout<<a Se produit avant a=11. Ce sont des opérations conflictuelles sur différents threads sans relation passe-avant; une course aux données.

41
bames53

Votre utilisation est correcte, mais insuffisante pour garantir quoi que ce soit d'utile.

Par exemple, le compilateur est libre d'implémenter en interne a = i; comme ça s'il veut:

 while(a != i)
 {
    ++a;
    atomic_thread_fence(std::memory_order_release);
 }

Ainsi, l'autre thread peut voir toutes les valeurs.

Bien sûr, le compilateur n'implémenterait jamais une simple affectation comme celle-là. Cependant, il existe des cas où un comportement tout aussi déroutant est en fait une optimisation, c'est donc une très mauvaise idée de s'appuyer sur le code ordinaire implémenté en interne d'une manière particulière. C'est pourquoi nous avons des choses comme les opérations atomiques et les clôtures ne produisent des résultats garantis que lorsqu'elles sont utilisées avec de telles opérations.

7
David Schwartz