C ++ memory_order_acquire / вопросы выпуска

Я недавно узнал о шести порядках памяти в С ++, меня очень смущали memory_order_acquire и memory_order_release, вот пример из cpp:

#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x() { x.store(true, std::memory_order_seq_cst); }
void write_y() { y.store(true, std::memory_order_seq_cst); }
 
void read_x_then_y() {
     while (!x.load(std::memory_order_seq_cst));

     if (y.load(std::memory_order_seq_cst)) 
         ++z;
}
 
void read_y_then_x() {
     while (!y.load(std::memory_order_seq_cst));

     if (x.load(std::memory_order_seq_cst))
        ++z;
}
 
int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);

    a.join(); b.join(); c.join(); d.join();

    assert(z.load() != 0);  // will never happen
}

На справочной странице cpp говорится:

Этот пример демонстрирует ситуацию, когда необходимо последовательное упорядочивание.

Любой другой порядок может вызвать утверждение, потому что потоки c и d могли бы наблюдать изменения в атомах x и y в противоположном порядке.

Итак, у меня вопрос: почему здесь нельзя использовать memory_order_acquire и memory_order_release? А какую семантику предоставляют memory_order_acquire и memory_order_release?

некоторые ссылки: https://en.cppreference.com/w/cpp/atomic/memory_order https://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync


person irving_ow    schedule 21.08.2020    source источник
comment
Некоторые ссылки: en.cppreference.com/w/cpp/atomic/memory_order   -  person KamilCuk    schedule 21.08.2020
comment
Обратите внимание, что переупорядочение StoreLoad без seq_cst продемонстрировать проще, чем IRIW; даже x86 с его строго упорядоченной моделью памяти может продемонстрировать StoreLoad в реальной жизни (preshing.com/20120515/memory-reordering-caught-in-the-act). На практике это все еще редко необходимо.   -  person Peter Cordes    schedule 21.08.2020


Ответы (2)


Последовательная согласованность обеспечивает единый общий порядок всех последовательно согласованных операций. Итак, если у вас есть последовательно согласованное хранилище в потоке A и последовательно согласованная загрузка в потоке B, а хранилище упорядочено до загрузки (в указанном едином общем порядке), тогда B наблюдает значение, сохраненное в A. Таким образом, в основном последовательная согласованность гарантирует, что хранилище будет немедленно видно другим потокам. Магазин релизов не предоставляет эту гарантию.

Как правильно указал Питер Кордес, термин «сразу видимый» довольно неточен. Видимость проистекает из того факта, что все операции seq-cst полностью упорядочены, и все потоки соблюдают этот порядок. Поскольку магазин и загрузка полностью упорядочены, значение магазина становится видимым до того, как будет выполнена последующая загрузка (в едином общем порядке).

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

Давайте посмотрим, что произошло бы, если бы мы использовали в этом примере получение / выпуск:

void write_x() { x.store(true, std::memory_order_release); }
void write_y() { y.store(true, std::memory_order_release); }
 
void read_x_then_y() {
     while (!x.load(std::memory_order_acquire));

     if (y.load(std::memory_order_acquire)) 
         ++z;
}
 
void read_y_then_x() {
     while (!y.load(std::memory_order_acquire));

     if (x.load(std::memory_order_acquire))
        ++z;
}
 
int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);

    a.join(); b.join(); c.join(); d.join();

    assert(z.load() != 0);  // can actually happen!!
}

Поскольку у нас нет гарантии видимости, может случиться так, что поток c наблюдает x == true и y == false, в то время как поток d может наблюдать y == true и x == false. Таким образом, ни один поток не будет увеличивать z, и утверждение сработает.

Для получения дополнительных сведений о модели памяти C ++ я могу порекомендовать эту статью, в которой я являюсь соавтором: Модели памяти для C / C ++ Программисты

person mpoeter    schedule 21.08.2020
comment
@PeterCordes, вы правы, но по моему опыту людям, которые только начинают пытаться осмыслить это, проще использовать более простые формулировки. уже видимый до загрузки в общем порядке операций seq_cst определенно более точен, но также и более абстрактен, и вы должны понимать концепцию общего порядка операций seq-cst. Однако я обновил свой ответ, чтобы прояснить это. - person mpoeter; 21.08.2020
comment
(переписал свой первый комментарий, потому что он касается IRIW, а не StoreLoad переупорядочение). сразу видимый - это оксюморон в многопоточности. Кроме того, очевидно, что это не имеет отношения к переупорядочиванию IRIW; мы не ищем StoreLoad, где хранилище выпуска, за которым следует загрузка, считывает значение до того, как хранилище станет видимым для других потоков. Непосредственная видимость, очевидно, не означает полного порядка, а это все, что имеет значение для IRIW. - person Peter Cordes; 21.08.2020
comment
Извините за путаницу, я просто догадался, не глядя на код, это будет StoreLoad, исходя из того, что сразу сказал о чем-то. Re: ваше обновление: нет последующей загрузки в потоках, выполняющих сохранение ›.‹ Я вижу, вы заметили, что во время 5-минутного окна редактирования, теперь выглядит хорошо. - person Peter Cordes; 21.08.2020
comment
@PeterCordes последующая загрузка относится к последующей загрузке в общем порядке. Пытался уточнить немного подробнее. - person mpoeter; 21.08.2020

Вы можете использовать aquire / release при передаче информации из одного потока в другой - это наиболее распространенная ситуация. Нет необходимости в последовательных требованиях к этому.

В этом примере есть несколько потоков. Два потока выполняют операцию записи, а третий грубо проверяет, был ли x готов до y, а четвертый проверяет, был ли y готов до x. Теоретически один поток может наблюдать, что x был изменен до y, а другой видит, что y был изменен до x. Не совсем уверен, насколько это вероятно. Это необычный вариант использования.

Изменить: вы можете визуализировать пример: предположим, что каждый поток выполняется на другом ПК, и они обмениваются данными через сеть. У каждой пары компьютеров разный пинг. Здесь легко привести пример, в котором неясно, какое событие произошло первым x или y, поскольку каждый компьютер будет видеть события, происходящие в разном порядке.

Я не уверен, на каких архитектурах может возникать этот эффект, но есть сложные, где соединены два разных процессора. Конечно, связь между процессорами медленнее, чем между ядрами каждого процессора.

person ALX23z    schedule 21.08.2020
comment
Да, это случай переупорядочения IRIW (независимые писатели, независимые читатели). Это возможно без seq_cst, но бывает только в реальной жизни на PowerPC. И это из-за пересылки хранилищ между потоками SMT на одном физическом ядре, что делает хранилища видимыми до того, как они станут видимыми глобально. Ничего общего с многоядерностью и многопроцессорностью; Кэш является согласованным, поэтому хранилища не могут фиксироваться в кеш-памяти L1d, пока все другие ядра не аннулируют свою копию. - person Peter Cordes; 21.08.2020