Модель памяти «получить-освободить» для двух последовательных атомарных операций

Следующий код записывает в атомарные переменные A, B и читает их в обратном порядке в другом потоке:

#include <atomic>
#include <thread>

// Initially.
std::atomic<int> A = std::atomic<int>(0);
std::atomic<int> B = std::atomic<int>(0);

// Thread 1
void write()
{
    A.store(1, std::memory_order_release); // line 1
    B.store(1, std::memory_order_release); // line 2
}

// Thread 2
void read()
{
    int valB = B.load(std::memory_order_acquire); // line 3
    int valA = A.load(std::memory_order_acquire); // line 4
}

int main()
{
    std::thread t1(write), t2(read);
    t1.join();
    t2.join();
    return 0;
}

В идеале, учитывая порядок выпуска-получения на атоме B, загрузка в строке 3 должна синхронизироваться с завершением сохранения в строке 1, так как это происходит до операции выпуска в строке 2. Но ссылка упоминает только (выделение , мой) -

Все операции записи в память (неатомарные и ослабленные атомарные), которые происходили до атомарного сохранения с точки зрения потока A, становятся видимыми побочными эффектами в потоке B.

Итак, порядок выпуска-приобретения для B не влияет на атомарные операции для других переменных, которые не являются ослабленным порядком памяти?

Возможно ли тогда иметь значения для valB = 1, valA = 0 после завершения read() в потоке 2, поскольку порядок памяти B не заботится о других атомарных операциях, которые не ослаблены? Нужна ли нам здесь последовательная согласованность?

Зафиксированный. Это должно было объяснить логику с помощью псевдокода. Я не думаю, что минимальный воспроизводимый пример помог бы здесь.

Akash 06.02.2023 12:41

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

Ted Lyngmo 06.02.2023 12:43

Чтобы не придираться к примеру, но… он по-прежнему в корне ошибочен — создание потока само по себе является точкой синхронизации. Если вы спрашиваете о «потоке 1» и «потоке 2», просто создайте 2 потока; вызов write() в основной теме на самом деле не короче и определенно меняет характер вопроса (непреднамеренно).

ildjarn 06.02.2023 12:53

@ildjarn что именно вы подразумеваете под созданием потока, это точка синхронизации? Не могли бы вы уточнить, в контексте текущего примера. Спасибо

Akash 06.02.2023 13:50

@Akash: Я бы не хотел, поэтому я написал комментарий, а не ответ. Пожалуйста, обратитесь к документации для конструктора std::thread, в которой указано, что завершение конструктора thread синхронизируется с началом вызова предоставленного вызываемого объекта в новом потоке. Таким образом, в показанном здесь коде работающий поток read совершенно не может видеть ничего, кроме 1 и 1, но не по причинам, о которых вы спрашиваете.

ildjarn 06.02.2023 13:57

@ildjarn Я понял твою точку зрения. Обновленный вопрос. Спасибо!

Akash 06.02.2023 14:42
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Возможно ли тогда иметь значения для valB = 1, valA = 0 после завершения read() в потоке 2, поскольку порядок памяти B не заботится о других атомарных операциях, которые не ослаблены?

Нет, это невозможно. Как вы процитировали:

Все операции записи в память (неатомарные и ослабленные атомарные), которые происходили до атомарного сохранения с точки зрения потока A, становятся видимыми побочными эффектами в потоке B.

Таким образом, когда вы читаете 1 из B, то, что произошло до записи в A, видно в потоке чтения, и, следовательно, следующее чтение из A также должно возвращать 1.

Выделенную вами часть в скобках можно было бы лучше сформулировать как «(включая неатомарные и расслабленно-атомарные)» — это то, что вас сбило с толку?

Что говорит стандарт?

Соответствующей частью стандарта будет atomics.order.1.4:

Атомарная операция A, которая выполняет операцию освобождения атомарного объекта M, синхронизируется с атомарной операцией B, которая выполняет операцию получения M.

Значение «синхронизируется с» описано в intro.multithread:

Оценка A между потоками происходит до оценки B, если A синхронизируется [...]

Оценка A происходит до оценки B (или, что то же самое, B происходит после A), если

  • A расположен перед B, или
  • Интерпоток происходит до B.

Происходит-перед является транзитивным: A.store() происходит-до (последовательность до) B.store(), происходит-до (синхронизируется-с) B.load(), происходит-до (последовательность до) A.load(). Таким образом, A.store() происходит раньше A.load().

В стандарте прямо говорится об атомарных объектах:

Если побочный эффект X на атомарном объекте M возникает до вычисления значения B для M, то оценка B должна брать свое значение из X или из побочного эффекта Y, который следует за X в порядке модификации M.

Итак, поскольку A.store(1) происходит до A.load() и нет других операций-кандидатов Y, A.load() должна возвращать 1.

Я также могу порекомендовать этот пост в блоге.

Да, в ссылке упоминаются только неатомарные операции, и, расслабленные атомарные операции, значит, я что-то неправильно читаю?

Akash 06.02.2023 12:43

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

Chronial 06.02.2023 12:48

Дополнил свой ответ соответствующими цитатами из стандарта, в которых это четко указано.

Chronial 06.02.2023 13:24

@Akash: Текст на cppreference неправильный/был неправильным и не говорит о том, что автор, вероятно, имел в виду (как говорит Chronial, они, вероятно, имели в виду «включая», но это не то, что они написали). Если бы B.store был relaxed, более ранний магазин release мог бы изменить заказ с ним, так что это на самом деле важное различие. (Даже более раннему хранилищу seq_cst будет разрешено переупорядочиваться, и это возможно при компиляции для AArch64.) (Модель памяти C++ формально определяется с точки зрения создания отношений «происходит до», а не с точки зрения локального переупорядочивания доступов к когерентному общему кешу, например мы обычно думаем.)

Peter Cordes 06.02.2023 18:48

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