параллелизм — атомарный прирост 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 () в каждом независимом потоке?

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

1

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]