Порядок памяти Acqrel с 3 потоками

В последнее время, чем больше я читаю о порядке памяти в C++, тем больше он запутывается. Надеюсь, вы поможете мне прояснить это (в чисто теоретических целях). Предположим, у меня есть следующий код:

std::atomic<int> val = { 0 };

std::atomic<bool> f1 = { false };
std::atomic<bool> f2 = { false };

void thread_1() {
  f1.store(true, std::memory_order_relaxed);

  int v = 0;
  while (!val.compare_exchange_weak(v, v | 1, 
                                    std::memory_order_release));
}

void thread_2() {
  f2.store(true, std::memory_order_relaxed);

  int v = 0;
  while (!val.compare_exchange_weak(v, v | 2, 
                                    std::memory_order_release));
}

void thread_3() {
  auto v = val.load(std::memory_order_acquire);
  if (v & 1) assert(f1.load(std::memory_order_relaxed));
  if (v & 2) assert(f2.load(std::memory_order_relaxed));
}

Возникает вопрос: может ли какое-либо из утверждений быть ложным? С одной стороны, cppreference утверждает, что std::memory_order_release запрещает переупорядочивать оба хранилища после обмена в потоках 1-2, а std::memory_order_acquire в потоке 3 запрещает переупорядочивать оба чтения до первой загрузки. Таким образом, если поток 3 увидел первый или второй установленный бит, это означает, что сохранение в соответствующее логическое значение уже произошло, и оно должно быть истинным.

С другой стороны, поток 3 синхронизируется с тем, кто освободил значение, полученное от val. Может ли так случиться (в теории, если не на практике), что поток 3 "приобрел" обмен "1 -> 3" потоком 2 (и поэтому f2 load возвращает true), но не "0 -> 1" потоком 1 (таким образом, срабатывает первое утверждение)? Эта возможность не имеет для меня смысла, учитывая понимание «переупорядочения», но я не могу найти никаких подтверждений того, что это не может произойти нигде.

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

LWimsey 15.03.2022 15:15
val.load может возвращать только 0,1,2 или 3. С последующими проверками во время выполнения порядок f1 и f2 (относительно их магазинов) гарантирован.
LWimsey 15.03.2022 15:20
Стоит ли изучать 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
2
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Ни одно из утверждений никогда не может потерпеть неудачу благодаря правилам ISO C++ «последовательности выпуска». Это формализм, который обеспечивает гарантию, что вы предположили, что она должна существовать в вашем последнем абзаце.

Единственными сохранениями в val являются выпускные хранилища с соответствующими установленными битами, выполняемые после соответствующего сохранения в f1 или f2. Так что, если thread_3 видит значение с установленным 1 битом, оно определенно синхронизировано с записывающим устройством, которое установило соответствующую переменную.

И что особенно важно, каждая из них является частью RMW и, таким образом, формирует выпуск-последовательность, который позволяет загрузке в thread_3 синхронизироваться с записью CAS оба, если это произойдет val == 3.

(Даже relaxed RMW может быть частью последовательности релиза, хотя в этом случае не будет гарантии «происходит до» для вещей до ослабленного RMW, только для других операций релиза этим или другими потоками для этой атомарной переменной. Если бы thread_2 использовал mo_relaxed, утверждение для f2 могло бы потерпеть неудачу, но оно все равно не могло бы сломать что-либо, поэтому утверждение для f1 могло бы когда-либо потерпеть неудачу. См. Также Что означает «последовательность выпуска»? и https://en.cppreference.com/w/cpp/atomic/memory_order)


Если это поможет, я считать, эти циклы CAS полностью эквивалентны val.fetch_or(1, release). Определенно именно так компилятор реализует fetch_or на машине с CAS, но не с атомарным примитивом ИЛИ. IIRC, в модели ISO C++, сбой CAS - это только нагрузка, а не RMW. Не то чтобы это имело значение; расслабленный RMW без операций все равно будет распространять последовательность выпуска.

(Забавный факт: x86 asm lock cmpxchg всегда является реальным RMW, даже в случае неудачи, по крайней мере, на бумаге. Но это также полный барьер, поэтому в основном не имеет отношения к любым рассуждениям о слабо упорядоченных RMW.)

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