Из статьи cppreference на std::memory_order
:
Синхронизируется с: Если атомарное сохранение в потоке A является операцией освобождения, атомарная загрузка в потоке B из той же переменной является операцией получения, а загрузка в потоке B считывает значение, записанное хранилищем в потоке A, то хранилище в потоке А синхронизируется с нагрузкой в потоке Б.
Имеет ли фраза «…читает значение, написанное…» точное значение, которое мне не хватает?
Каждый атомарный объект имеет (индивидуально) общий порядок своих модификаций, так называемый порядок модификации, который все потоки должны глобально согласовать.
Каждый побочный эффект (т. е. сохранение) атомарного объекта составляет запись в этом порядке модификации.
Каждое вычисление значения (т. е. загрузка) атомарного объекта берет свое значение из одного из побочных эффектов в этом порядке модификации, который, за исключением некоторых правил согласованности, является неопределенным.
Эти правила согласованности приведены на странице cppreference, на которую вы ссылаетесь:
Следующие четыре требования гарантируются для всех атомарных операций:
Согласованность записи-записи: если оценка A, которая изменяет некоторый атомарный M (запись) происходит - до оценки B, которая изменяет M, затем появляется A раньше, чем B, в порядке модификации M.
Когерентность чтения-чтения: если вычисление значения A некоторого атома M (a чтение) происходит — до вычисления значения B на M, и если значение A происходит в результате записи X на M, то значение B является либо значением сохраненное X, или значение, сохраненное побочным эффектом Y на M, которое появляется позже, чем X, в порядке модификации M.
Когерентность чтения-записи: если вычисление значения A некоторого атомарного M (a чтение) происходит — перед операцией B над M (записью), тогда значение A возникает в результате побочного эффекта (записи) X, который появляется раньше, чем B в порядок внесения изменений М.
Согласованность записи-чтения: если побочный эффект (запись) X на атомном происходит объект M — до вычисления значения (чтения) B из M, тогда Оценка B должна принимать свое значение от X или от побочного эффекта Y, который следует за X в порядке модификации M.
Обратите внимание, что вышеприведенное использует неформальный язык, такой как «значение A берется из», что формально должно быть «вычисление значения A берет свое значение из». Официальную спецификацию можно найти в [intro.races]/14 и последующих параграфах.
«и загрузка в потоке B считывает значение, записанное хранилищем в потоке A» — это также менее формальный способ сказать «и вычисление значения в потоке B берет свое значение из побочного эффекта в потоке A в порядке модификации атомный объект».
В вашей цитате говорится, что отношение синхронизируется с возникает только при конкретной оценке, которая вызвала побочный эффект для атомарного объекта, из которого вычисление значения берет свое значение.
В частности, это означает, что если в порядке модификации есть два побочных эффекта, оба из которых сохраняют одно и то же значение в атомарном элементе, то, принимая во внимание приведенные выше правила согласованности, возможно, что вычисление значения в атомарном элементе займет его значение от одного из двух. Хотя вы не можете различить эти два случая, глядя на значение, создаваемое загрузкой, это все равно повлияет на отношение синхронизации с и на то, какие гарантии синхронизации памяти вы получите.
Как указывает @PeterCordes в комментарии к этому ответу, правило, когда выпуск/получение подразумевает синхронизацию с кавычками из cppreference, также является неполным. На самом деле отношение синхронизации с устанавливается не только с помощью оценки, которая вызвала побочный эффект, из которого вычисление значения принимает свое значение.
Вместо этого отношение существует также, если вычисление значения берет свое значение из любого побочного эффекта в так называемой последовательности выпуска, возглавляемой рассматриваемой операцией выпуска, которая является непрерывной подпоследовательностью порядка модификации, состоящего из операций выпуска, начинающихся с операции выпуска. на рассмотрении.
Это означает, что если значение берется из какой-либо операции освобождения в порядке модификации и перед ней есть другая операция освобождения, то, как правило, операция получения также синхронизируется с предыдущей операцией освобождения.
Но из-за некоторых необычных моделей памяти ЦП это не применимо ко всем операциям предыдущих выпусков. Текущее определение последовательности релизов можно найти в [intro.races]/5, и по этому поводу ведутся (постоянные?) дискуссии. В частности, текущее определение содержит только операции освобождения, которые также являются операциями чтения-изменения-записи.
Наивно можно было бы ожидать, что отношениеsyncs-with будет применяться ко всем предыдущим хранилищам релизов в порядке модификации, поэтому это может привести к некоторым неожиданным результатам.
Например, рассмотрим этот сценарий:
0
1
в него после записи некоторых данных A для публикации1
с ослабленным порядком памяти, затем записывает (в другом месте) некоторые другие данные B для публикации и выпуска-сохранения 2
в атомарную переменную (не с приращением RMW!).2
из хранилища потока 2.Тогда поток 3 сможет безопасно получить данные B, но поскольку модификация, не связанная с RMW, не является частью последовательности выпуска, никакой синхронизации с потоком 1 не будет, и доступ к данным A будет представлять собой гонку данных.
Интересный факт: на большинстве реального оборудования загрузка получения синхронизируется со всеми операциями более раннего выпуска в порядке модификации. Но по крайней мере в одном (я думаю, PowerPC) есть крайний случай, когда это не так, поэтому (в позднем проекте C++11) они ослабили правила синхронизации, чтобы синхронизироваться только с последовательностью релизов (хранилище релизов). и любое количество атомных RMW). Что означает «последовательность релизов»? . И open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0668r1.html указывает, что текущее определение не идеально подходит для программистов или архитекторов ЦП.
Слова до и после важны: «загрузка... читает значение, записанное хранилищем». Здесь поток B загружает/получает сохраненное/освобождает значение и видит новое значение, поэтому синхронизируется; после загрузки поток B может безопасно прочитать все данные, записанные потоком A перед сохранением. Если хранилище не было «освобождено» или загрузка не была «получена», обновление данных может появиться в потоке B в любом порядке.