Zrozumienie pamięci c++11

Próbuję zrozumieć płoty pamięci w c++11, wiem, że są lepsze sposoby na to, zmienne atomowe itd., ale zastanawiam się, czy to użycie było poprawne. Zdaję sobie sprawę, że ten program nie robi nic użytecznego, chciałem się tylko upewnić, że korzystanie z funkcji ogrodzenia zrobił to, co myślałem, że zrobili.

Zasadniczo, że zwolnienie zapewnia, że wszelkie zmiany dokonane w tym wątku przed ogrodzeniem są widoczne dla innych wątków po ogrodzeniu, a w drugim wątku, który jakieś zmiany w zmiennych są widoczne w wątku zaraz po ogrodzeniu?

Czy moje rozumienie jest poprawne? A może całkowicie przeoczyłem sedno?
#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();
}
Author: Nicol Bolas, 2012-11-29

2 answers

Twoje użycie Nie Nie w rzeczywistości zapewnia rzeczy, o których wspominasz w komentarzach. Oznacza to, że korzystanie z ogrodzeń nie zapewnia, że przypisania do a są widoczne dla innych wątków lub że wartość odczytana z a jest ' up to date."Dzieje się tak dlatego, że chociaż wydaje się, że masz podstawową wiedzę o tym, gdzie należy używać ogrodzeń, Twój kod nie spełnia dokładnych wymagań dotyczących "synchronizacji" tych ogrodzeń.

Oto inny przykład, który myślę lepiej pokazuje poprawne użycie.

#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();
}

Ładowanie i przechowywanie na atomic flag nie synchronizują się, ponieważ obie używają luźnej kolejności pamięci. Bez ogrodzeń ten kod byłby wyścigiem danych, ponieważ wykonujemy sprzeczne operacje obiekt niematomowy w różnych wątkach, a bez ogrodzeń i synchronizacji, które zapewniają, nie byłoby związku happens-before pomiędzy konfliktowymi operacjami na a.

Jednak z ogrodzeniami robimy uzyskaj synchronizację, ponieważ zagwarantowaliśmy, że wątek 2 będzie odczytywał flagę napisaną przez wątek 1 (ponieważ pętlę, dopóki nie zobaczymy tej wartości), a ponieważ zapis atomowy miał miejsce po ogrodzeniu release, a odczyt atomowy ma miejsce-przed ogrodzeniem acquire, ogrodzenia synchronizują się. (patrz § 29.8/2 dla szczególnych wymagań.)

Ta synchronizacja oznacza wszystko, co się dzieje-przed wydarzeniem ogrodzenia zwalniającego-przed wszystkim, co się wydarzy-po ogrodzeniu nabycia. Dlatego nieatomowe zapis do a happens-przed nieatomowym odczytem a.

Rzeczy stają się trudniejsze, gdy piszesz zmienną w pętli, ponieważ możesz ustanowić relację happens-before dla określonej iteracji, ale nie dla innych iteracji, powodując wyścig danych.

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';
    }
}

Ten kod nadal powoduje synchronizację ogrodzeń, ale nie eliminuje wyścigów danych. Na przykład jeśli f.load() zwróci 10 to wiemy, że a=1,a=2, ... a=10 czy wszystko się wydarzyło-zanim ten konkretny cout<<a, ale nie wiemy, że cout<<a dzieje się-przed a=11. Są to sprzeczne operacje na różnych wątkach bez relacji happens-before; wyścig danych.

 33
Author: bames53,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-07 05:53:06

Twoje użycie jest poprawne, ale niewystarczające, aby zagwarantować cokolwiek użytecznego.

Na przykład kompilator może zaimplementować wewnętrznie a = i; w ten sposób, jeśli chce:

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

Więc drugi wątek może zobaczyć dowolne wartości.

Oczywiście kompilator nigdy nie zaimplementowałby takiego prostego zadania. Istnieją jednak przypadki, w których podobnie kłopotliwe zachowanie jest w rzeczywistości optymalizacją, więc bardzo złym pomysłem jest poleganie na zwykłym kodzie zaimplementowanym wewnętrznie w jakikolwiek szczególny sposób. Dlatego mamy takie rzeczy jak operacje atomowe i ogrodzenia tylko dają gwarantowane wyniki, gdy są używane z takimi operacjami.

 6
Author: David Schwartz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-29 19:05:32