Приращение атома в C++ с упорядочением памяти

После того, как я прочитал параллелизм C++ в действии, Глава 5, я попытался написать некоторый код, чтобы проверить мое понимание порядка памяти:

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

std::atomic<int> one,two,three,sync;

void func(int i){
    while(i != sync.load(std::memory_order_acquire));
    auto on = one.load(std::memory_order_relaxed); ++on;
    auto tw = two.load(std::memory_order_relaxed); ++tw;
    auto th = three.load(std::memory_order_relaxed); ++th;
    std::cout << on << tw << th << std::endl;
    one.store(on,std::memory_order_relaxed);
    two.store(tw,std::memory_order_relaxed);
    three.store(th,std::memory_order_relaxed);
    int expected = i;
    while(!sync.compare_exchange_strong(expected,i+1,
            std::memory_order_acq_rel))
        expected = i;
}

int main(){
    std::vector<std::thread> t_vec;
    for(auto i = 0; i != 5; ++i)
        t_vec.push_back(std::thread(func,i));
    for(auto i = 0; i != 5; ++i)
        t_vec[i].join();
    std::cout << one << std::endl;
    std::cout << two << std::endl;
    std::cout << three << std::endl;
    return 0;
}

Мой вопрос: книга говорит, что memory_order_release и memory_order_acquire должны быть парой, чтобы правильно прочитать правильное значение.

Поэтому, если первая строка функции func() - это синхронизация нагрузки в цикле с memory_order_acquire, она должна разорвать пару и сделать непредсказуемую ошибку при синхронизации.

Однако, как и ожидалось, он печатается после компиляции на моей платформе x86:

111
222
333
444
555
5
5
5

Результат показывает без проблем. Так что мне просто интересно, что происходит внутри func() (хотя я написал это сам...)?

Добавлено: В соответствии с кодом на C++ параллелизм в действии страница 141:

#include <atomic>
#include <thread>

std::vector<int> queue_code;
std::atomic<int> count;

void populate_queue(){
    unsigned const number_of_items = 20;
    queue_data.clear();
    for(unsigned i = 0; i < number_of_items; ++i)
        queue_data.push_back(i);
    count.store(number_of_items, std::memory_order_release);
}

void consume_queue_items(){
    while(true){
        int item_index;
        if((item_index=count.fetch_sub(1,memory_order_acquire))<=0){
            wait_for_more_items();
            continue;
        }
        process(queue_data[item_index-1]);
    }
}

int main(){
    std::thread a(populate_queue);
    std::thread b(consume_queue_items);
    std::thread c(consume_queue_items);
    a.join();
    b.join();
    c.join();
}

Поток b и поток c будут работать нормально, независимо от того, кто будет считать счет первым. Так как:

К счастью, первый fetch_sub() действительно участвует в последовательности выпуска, и поэтому store() синхронизируется со вторым fetch_sub(). Относительно синхронизации между двумя пользовательскими потоками по-прежнему нет синхронизации. В цепочке может быть любое количество ссылок, но при условии, что все они выполняют операцию чтения-изменения-записи, такую ​​как fetch_sub(), store() все равно будет синхронизироваться с каждый из которых помечен memory_order_acquire. В этом примере все ссылки одинаковы, и все они являются операциями получения, но они могут представлять собой сочетание различных операций с различной семантикой memory_ordering.

Но я не могу найти связанную информацию об этом, и как операция чтения-изменения-записи, такая как fetch_sub(), участвует в последовательности выпуска? Если я изменю его на загрузку с memory_order_acquire, будет ли store() по-прежнему синхронизироваться с load() в каждом независимом потоке?

1 ответ

Решение

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

Упорядочение памяти правильное и даже более сильное, чем это технически необходимо. compare_exchange_strong внизу не обязательно; равнина store с барьером выпуска будет достаточно:

sync.store(i+1, std::memory_order_release);

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

Поэтому, если первая строка функции func() - это синхронизация нагрузки в цикле с memory_order_acquire, она должна разорвать пару и сделать непредсказуемую ошибку при синхронизации.

Спаривание захвата / выпуска правильное, поскольку барьер разблокировки внизу (в резьбе X) соединяется с барьером захвата вверху (в резьбе Y). То, что первый поток получает без предыдущего выпуска, это хорошо, так как выпускать еще нечего.

Одобавленной части:

Как операция чтения-изменения-записи, такая как fetch_sub(), участвует в последовательности выпуска?

Вот что стандарт говорит в 1.10.1-5:

Последовательность освобождения, возглавляемая операцией освобождения атомарного объекта M, представляет собой максимальную непрерывную подпоследовательность побочных эффектов в порядке модификации M, где первой операцией является A, и каждой последующей операцией:

  • выполняется тем же потоком, который выполнил A, или
  • является атомарной операцией чтения-изменения-записи.

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

Если я изменю его на загрузку с memory_order_acquire, будет ли store() по-прежнему синхронизироваться с load() в каждом независимом потоке?

Если вы измените чтение-изменение-запись на отдельную загрузку / получение (которая видит обновленное значение) и сохранение / выпуск, это все равно правильно, но больше не является частью той же последовательности выпуска; Вы создали отдельную последовательность релизов.

Другие вопросы по тегам