Есть ли у нас гарантия, что любая атомарная запись немедленно сохранит новое значение атомарной переменной в основной памяти?

Итак, я много читал о переупорядочении инструкций и памяти и о том, как мы можем это предотвратить, но у меня до сих пор нет ответа на один вопрос (вероятно, потому, что я недостаточно внимателен). Мой вопрос: есть ли у нас гарантия, что любая атомарная запись немедленно сохранит новое значение атомарной переменной в основной памяти? Давайте посмотрим на небольшой пример:

std::atomic<bool> x;
std::atomic<bool> y;
std::atomic<int> count;
void WritingValues()
{
   x.store(true, std::memory_order_relaxed);
   y.store(true, std::memory_order_relaxed);
}
void ReadValues()
{
   while( !y.load(std::memory_order_relaxed) );
   if( x.load(std::memory_order_relaxed) )
       ++count;
}
int main()
{
   x = false;
   y = false;
   count = 0;
   std::thread tA(WritingValues);
   std::thread tB(ReadValues);
   tA.join();
   tB.join();
   assert( count.load() != 0 );
}

Итак, здесь наше утверждение может определенно сработать, поскольку мы используем std :: memory_order_relaxed и не предотвращаем переупорядочение инструкций (или переупорядочение памяти во время компиляции, я полагаю, это одно и то же). Но если мы поместим некоторый барьер компилятора в WritingValues, чтобы предотвратить переупорядочение инструкций, все ли будет в порядке? Я имею в виду, гарантирует ли x.store (true, std :: memory_order_relaxed), что запись этой конкретной атомарной переменной будет осуществляться непосредственно в память без какой-либо задержки? Или x.load (std :: memory_order_relaxed) гарантирует, что значение будет считано из памяти, а не из кеша с недопустимым значением? Другими словами, это хранилище гарантирует только атомарность операции и имеет то же поведение памяти, что и обычная неатомарная переменная, или оно также влияет на поведение памяти?


person IgnatiusPo    schedule 09.08.2019    source источник
comment
Ваш вопрос на самом деле бессмысленен, и на него нельзя ответить. Когда дело доходит до C ++, правильная концепция - это секвенирование. В том же потоке выполнения, изменяя одну атомарную переменную за другой, одна последовательность первая за второй. Но ничто из этого не будет определять, наблюдает ли другой поток выполнения изменения какой-либо из этих атомарных переменных. Чтобы определить это, необходимо определить, как эти потоки выполнения связаны друг с другом. И для этого тоже есть несколько правил.   -  person Sam Varshavchik    schedule 09.08.2019
comment
Я не совсем уверен, что понимаю вас, @SamVarshavchik. Что вы имеете в виду, изменяя одну атомарную переменную за другой, одна за другой, одна за другой. А как насчет переупорядочения инструкций? Насколько я знаю, в этом конкретном случае это легко может произойти. Также меня немного смущает эта связка правил. Один из возможных вариантов сделать это правильно - использовать барьеры памяти, об этом я и говорю. Я просто хочу знать, есть ли вероятность, что атомная запись не будет записывать в память немедленно.   -  person IgnatiusPo    schedule 09.08.2019
comment
Все, что гарантирует атомарная запись, - это то, что никакой другой поток не увидит частичное содержимое объекта. Другие потоки видят значение до или после его изменения. Для bools это в основном бессмысленно. Что касается того, видят ли другие потоки запись немедленно, это навсегда останется загадкой, потому что поток ничего не может сделать, чтобы определить, написал ли что-то другой поток. Это была бы последовательность. I.E .: после блокировки мьютекса поток увидит все, что сделал другой поток, прежде чем разблокировать тот же мьютекс, потому что это так упорядочено.   -  person Sam Varshavchik    schedule 09.08.2019
comment
@SamVarshavchik А как насчет протокола MESI / MOESI? Я думал, что это было разработано специально для таких случаев, когда возникают проблемы с когерентностью кеша. Таким образом, поток знает, была ли изменена память, поскольку у нее есть специальный модификатор. В любом случае, я думаю, мы говорим о разных вещах, или я просто вас неправильно понял.   -  person IgnatiusPo    schedule 15.08.2019
comment
Кажется, ваш вопрос смешивает CPU и C / C ++. Вы не можете сделать это напрямую: семантика C / C ++ не указана в терминах низкоуровневых гарантий asm и ЦП (это вообще не указано, но это уже другая тема). Вам нужно вставить границу ABI где-нибудь, чтобы смешивать высокий и низкий уровень.   -  person curiousguy    schedule 16.08.2019
comment
@curiousguy Да, я понимаю. Но для этого ли существуют барьеры памяти, не так ли? Я имею в виду, что на разных платформах эти барьеры памяти работают по-разному. (например, в x86 порядок последовательной согласованности ведет себя так же, как порядок acq_rel) Таким образом, эти барьеры заставляют компилятор гарантировать нам в некоторых случаях. Я не знаю, что именно используют компиляторы, какие инструкции asm, но в этом-то и дело - если я правильно использую барьеры памяти, я могу быть уверен, что память видна там, где я хочу, так что это своего рода смесь CPU и C ++, не так ли? не так ли? Я просто хочу прояснить это, поправьте меня, если я ошибаюсь   -  person IgnatiusPo    schedule 16.08.2019
comment
@IgnatiusPo На практике это может быть так, но на абстрактном уровне C / C ++ вообще не определяется в терминах CPU. Компилятору нужно только соответствовать низкоуровневому материалу ЦП 1) для энергозависимого доступа 2) на границах ABI, когда вы вызываете другую функцию в соответствии с ABI. Что вы имеете в виду под CS ведет себя как ack-rel?   -  person curiousguy    schedule 16.08.2019
comment
@curiousguy Я имел в виду, что многое с памятью на архитектуре процессора x86 делается по самым строгим правилам, и в качестве примера - с упорядочением памяти. x86 известен как очень строгий парень, поэтому по умолчанию он будет делать все самым строгим образом, используя самые строгие инструкции asm и так далее. Существует разница между порядком seq_cst и acq_rel (ссылка), но не для x86. Я понимаю, что смешиваю низкоуровневые ресурсы ЦП с C ++, но в конечном итоге все эти барьеры памяти заставляют компилятор вызывать некоторые конкретные инструкции asm.   -  person IgnatiusPo    schedule 16.08.2019
comment
@curiousguy Но я понял вашу точку зрения, спасибо. Я здесь новичок и не уверен, что четко выражаю свои мысли :)   -  person IgnatiusPo    schedule 16.08.2019
comment
@IgnatiusPo Какой барьер необходим на x86 для получения заказа acq_rel?   -  person curiousguy    schedule 16.08.2019
comment
@curiousguy Если вы хотите использовать упорядочение памяти acq_rel, вы используете std :: memory_order_acq_rel барьер (или std :: memory_order_acquire при чтении и std :: memory_order_release при записи той же атомарной переменной), но на x86 вы будете получите такой же результат, как если бы вы использовали std :: memory_order_seq_cst барьер.   -  person IgnatiusPo    schedule 16.08.2019
comment
@IgnatiusPo Какой asm создается барьером memory_order_acq_rel?   -  person curiousguy    schedule 16.08.2019
comment
@curiousguy Я бы сказал, что ты, если бы знал. Я полностью уверен, что это зависит от платформы, и я не знаю, что именно они используют. Я слышал, что x86 очень строгий из здесь и здесь   -  person IgnatiusPo    schedule 19.08.2019
comment
Упоминание об основной памяти проблематично, поскольку данные не нужно помещать в оперативную память в любое время, когда кеши достаточно велики; Вы как будто делаете предположение о работе системы памяти. При необходимости данные из одного кэша можно скопировать в другой кэш или основную оперативную память; это вне контроля программы. (Если вы не аннулируете кеш, что редко требуется.)   -  person curiousguy    schedule 22.08.2019
comment
@curiousguy Да, это то, что означает протокол MESI / MOESI   -  person IgnatiusPo    schedule 23.08.2019


Ответы (2)


 I mean, does x.store(true, std::memory_order_relaxed) guarantees, that the  
 of that particular atomic variable will be directly into the memory,  
 without any latency?  

Нет, это не так, и на самом деле при ослабленном порядке bool и памяти нет «недопустимого» значения, если вы читаете его только один раз, оба значения - true и false.
Поскольку ослабленный порядок памяти явно остается, упорядочение не выполняется. В основном в вашем случае это означает только то, что после переключения с false на true в какой-то момент он станет истинным для всех других процессов, но не указывает, «когда» это произойдет. Так что единственное, в чем вы можете быть уверены, это то, что оно больше не станет ложным после того, как станет истинным. Но нет никаких ограничений на то, как долго он будет ложным в другом потоке.
Также он гарантирует, что вы не увидите частично записанную переменную в другом потоке, но это вряд ли относится к bools.
Вам нужно использовать здесь получение и выпуск. И даже это не даст никаких гарантий относительно фактической памяти, только о поведении программы, синхронизация кеша может помочь даже без возврата данных и пены в память.

person alexrider    schedule 09.08.2019
comment
Да, для этого существуют выпуск и приобретение порядка памяти. Написание с релизом освобождает его для всех, кто читает это с усвоением. - person alexrider; 09.08.2019
comment
Release только освобождает другие, предыдущие операции с памятью. То, что выполняется операцией выпуска, не затрагивается. - person curiousguy; 21.01.2020

Поскольку все инструкции загрузки и сохранения являются атомарными, каждая из них является отдельной машинной инструкцией, поэтому два потока никогда не «прерывают друг друга» в «середине» инструкции загрузки или сохранения.

Заголовок вашего вопроса: «Есть ли у нас гарантия, что любая атомарная запись немедленно сохранит новое значение атомарной переменной в основной памяти?». Но само определение атомарной инструкции состоит в том, что она не может быть прервана переключением контекста, аппаратное прерывание, программное ожидание - ничего!

std::memory_order_relaxed позволяет изменить порядок инструкций в одной функции. См., Например, этот вопрос. Это почти то же самое, что и ваш вопрос, но у вас memory_order_relaxed в ReadValues() вместо memory_order_acquire. В этой функции возможна установка спин-блокировки переменной y после приращения счетчика из-за ослабленного условия (переупорядочение компилятора и т. Д.). В любом случае ASSERT может завершиться ошибкой, потому что y может быть установлено в true до того, как x окажется в WriteValues() из-за переупорядочения памяти, разрешенного memory_order_relaxed (ссылка на ответьте на аналогичный вопрос.

person Ð..    schedule 09.08.2019
comment
Ага, я написал то же самое в вопросе: D. Я знаю, что assert может сработать, и я заметил это выше. Также я не прошу перебивать, это понятно. Я не был уверен в согласованности кеша и поведении памяти здесь, я думал, что атомарная переменная предотвращает проблему переупорядочения памяти при любом указанном порядке памяти, но я ошибался. Теперь я знаю ответ, спасибо :) - person IgnatiusPo; 09.08.2019