Подтвердить что ты не робот

Понимание памяти С++ 11

Я пытаюсь понять память в С++ 11, я знаю, что есть лучшие способы сделать это, атомные переменные и т.д., но задавался вопросом, правильно ли это использование. Я понимаю, что эта программа не делает ничего полезного, я просто хотел убедиться, что использование функций забора сделало то, что я думал, что они сделали.

В основном, что релиз гарантирует, что любые изменения, сделанные в этом потоке перед ограждением, видны другим нитям после забора, а во втором потоке, что любые изменения в переменных видны в потоке сразу после забора?

Правильно ли я понимаю? Или я полностью упустил точку?

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

Ответ 1

Ваше использование фактически не гарантирует того, что вы упомянули в своих комментариях. То есть ваше использование забора не гарантирует, что ваши назначения a видны другим потокам или что значение, которое вы читаете с a, является "актуальным". Это связано с тем, что, хотя у вас, похоже, есть основная идея того, где следует использовать ограждения, ваш код на самом деле не соответствует точным требованиям для этих ограждений, чтобы "синхронизировать".

Вот другой пример, который, я думаю, лучше показывает правильное использование.

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

Загрузка и сохранение на атомном флаге не синхронизируются, потому что они оба используют упорядоченное упорядочение памяти. Без ограждений этот код будет представлять собой гонку данных, потому что мы выполняем конфликтующие операции неатомным объектом в разных потоках, и без заборов и синхронизации, которые они обеспечивают, не будет происходить - до отношения между конфликтующими операциями на a.

Однако с забором мы получаем синхронизацию, потому что мы гарантировали, что поток 2 прочитает флаг, написанный потоком 1 (потому что мы зацикливаемся до тех пор, пока не увидим это значение), и так как атомная запись произошла после забора релиза и происходит атомарное считывание - до получения забора, заграждения синхронизируются. (см. § 29.8/2 для конкретных требований.)

Эта синхронизация означает все, что происходит - до того, как произойдет освобождение забора - перед чем-либо, что происходит - после забора. Поэтому неатомная запись в a происходит до неатомного чтения a.

Все становится сложнее, когда вы пишете переменную в цикле, потому что вы можете установить связь между случаями и событиями для какой-либо конкретной итерации, но не другие итерации, вызывающие гонку данных.

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

Этот код по-прежнему заставляет ограждения синхронизировать, но не устраняет расы данных. Например, если f.load() происходит с возвратом 10, то мы знаем, что a=1, a=2,... a=10 все произошло - до этого конкретного cout<<a, но мы не знаем, что cout<<a происходит до a=11. Это конфликтующие операции на разных потоках без каких-либо связей; гонка данных.

Ответ 2

Ваше использование верное, но недостаточное, чтобы гарантировать что-либо полезное.

Например, компилятор может свободно реализовывать a = i; таким образом, если он хочет:

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

Таким образом, другой поток может видеть любые значения.

Конечно, компилятор никогда не выполнил бы такое простое назначение. Тем не менее, бывают случаи, когда подобное поведение, похожее на недоумение, на самом деле является оптимизацией, поэтому очень плохой идеей полагаться на обычный код, реализуемый внутри себя каким-либо определенным образом. Вот почему у нас есть такие вещи, как атомные операции и заборы, которые дают только гарантированные результаты при использовании таких операций.