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

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

#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 () действительно участвует в последовательности release , поэтому store () синхронизируется со второй fetch_sub (). Между двумя потоками-потребителями по-прежнему нет взаимосвязи «синхронизируется с». В цепочке может быть любое количество ссылок, но при условии, что они все операции чтения-изменения-записи, такие как fetch_sub (), store () все равно будет синхронизироваться с каждая из них помечена как memory_order_acquire. В этом примере все ссылки одинаковы и все являются операциями получения, но они могут быть смесью разных операций с разной семантикой memory_ordering.

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


person FXTi    schedule 11.08.2017    source источник
comment
Неопределенное поведение не определено. Ожидаемое наблюдаемое поведение - это одна из возможных вещей, которые могут произойти с неопределенным поведением.   -  person SergeyA    schedule 11.08.2017
comment
@Jeff Значит, нужен тест на другой платформе? Нравится ARM? Что делать, если у меня нет подходящей тестовой платформы ... Кстати, как правильно реализовать   -  person FXTi    schedule 11.08.2017
comment
На самом деле я не вижу состояния гонки с вашим кодом. Потоки будут правильно сериализованы, потому что синхронизация всегда записывается с memory_order_acq_rel, которая синхронизируется с чтением с memory_order_acquire. Какую проблему вы бы ожидали? Я не понимаю код, который вы предлагаете в разделе "Добавлено".   -  person PaulR    schedule 11.08.2017
comment
@PaulR ОК Я расскажу подробнее о Добавленной части   -  person FXTi    schedule 12.08.2017


Ответы (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:

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

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

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

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

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

person LWimsey    schedule 11.08.2017
comment
Таким образом, вы имеете в виду, что, поскольку барьер освобождения внизу (в потоке X) соединяется с барьером захвата вверху (в потоке Y), барьер захвата внизу (в потоке Y) не разрывает пару потоков Y и Z. И если пара устанавливает цепочку из пяти потоков, переменные будут показывать правильные значения во всех потоках? Приведенный выше код предназначен только для тестирования. Я расскажу подробнее о добавленной части и благодарю за ответ! - person FXTi; 12.08.2017
comment
Барьер приобретения в нижней части кода не имеет смысла. В этот момент вы «освобождаете» данные, чтобы заказать их для следующего потока, который их «получит». Каждый из ваших потоков получает данные после последнего; потоки выполняются последовательно. Посмотрите в книге отрывок об отношении «синхронизируется с». Здесь есть более подробное описание того, как это работает. - person LWimsey; 12.08.2017
comment
Итак ... что насчет добавленной части? - person FXTi; 12.08.2017
comment
Завтра я дополню свой ответ некоторой информацией о последовательностях выпуска ... В следующий раз, когда вы добавите это много к исходному вопросу, лучше задать новый вопрос. - person LWimsey; 12.08.2017
comment
Значит, fetch_sub () с порядком получения предназначен для повышения производительности, а порядок acq_rel здесь также будет работать? - person FXTi; 13.08.2017
comment
fetch_sub с acq_rel - более сильный барьер, чем необходимо. В этом нет ничего плохого, но вам это и не нужно. Барьер приобретения - это нормально. - person LWimsey; 14.08.2017