Использование атомарной операции чтения-изменения-записи в последовательности выпуска

Скажем, я создаю объект типа Foo в потоке №1 и хочу иметь к нему доступ в потоке №3.
Я могу попробовать что-то вроде:

std::atomic<int> sync{10};
Foo *fp;

// thread 1: modifies sync: 10 -> 11
fp = new Foo;
sync.store(11, std::memory_order_release);

// thread 2a: modifies sync: 11 -> 12
while (sync.load(std::memory_order_relaxed) != 11);
sync.store(12, std::memory_order_relaxed);

// thread 3
while (sync.load(std::memory_order_acquire) != 12);
fp->do_something();
  • Магазин / выпуск в потоке №1 заказывает Foo с обновлением до 11
  • поток # 2a неатомарно увеличивает значение sync до 12
  • связь синхронизирует с между потоками №1 и №3 устанавливается только тогда, когда №3 загружает 11

Синхронизация Foo

Сценарий нарушен, потому что поток № 3 вращается до тех пор, пока не загрузит 12, которые могут поступить не по порядку (по сравнению с 11), а Foo не упорядочен с 12 (из-за ослабленных операций в потоке № 2a).
Это несколько противоречит -интуитивно, поскольку порядок модификации sync 10 → 11 → 12

Стандарт гласит (§ 1.10.1-6):

атомарное освобождение хранилища синхронизируется с получением загрузки, которое принимает значение из хранилища (29.3). [Примечание: за исключением указанных случаев, чтение более позднего значения не обязательно обеспечивает видимость, как описано ниже. Такое требование иногда мешает эффективной реализации. - конец примечания]

Также в (§ 1.10.1-5) говорится:

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

Теперь поток № 2a модифицирован для использования атомарной операции чтения-изменения-записи:

// thread 2b: modifies sync: 11 -> 12
int val;
while ((val = 11) && !sync.compare_exchange_weak(val, 12, std::memory_order_relaxed));

Если эта последовательность выпуска верна, Foo синхронизируется с потоком № 3 при загрузке 11 или 12. Мои вопросы об использовании атомарного чтения-изменения-записи:

  • Является ли сценарий с потоком 2b правильной последовательностью выпуска?

И если это так:

  • Каковы особые свойства операции чтения-изменения-записи, обеспечивающие верность этого сценария?

person LWimsey    schedule 15.08.2017    source источник
comment
Есть ли у вас какие-то особые причины сомневаться в том, что store(11) и compare_exchange(11, 12) составляют последовательность выпуска? Они удовлетворяют всем требованиям в процитированном вами абзаце.   -  person Anton    schedule 16.08.2017
comment
@ user3290797 Ну, может быть, потому что я раньше видел эти цепочки с RMW в конце, но никогда в середине. Вы правы, он должен быть правильным по стандарту. Думаю, это больше касается уточняющих вопросов.   -  person LWimsey    schedule 16.08.2017
comment
RMW особенный, потому что часть магазина не может быть впереди части загрузки. Условные переходы / спекулятивное выполнение могут позволить sync.store(12, mo_relaxed); выполниться и стать глобально видимым до того, как цикл вращения фактически загрузит 11, нарушая причинно-следственную связь. Не может быть управляющей зависимости как части реализации атомарного RMW, только истинная зависимость данных от загрузки к хранилищу, поэтому она не может нарушать причинно-следственную связь таким образом (или любым другим способом из-за этого правила C ++, разрешающего атомарный RMW будет частью последовательности релизов!)   -  person Peter Cordes    schedule 01.09.2017
comment
Я, вероятно, должен опубликовать это как ответ, но я не уверен на 100%, что мои рассуждения верны.   -  person Peter Cordes    schedule 01.09.2017
comment
@PeterCordes Я интерпретирую сценарий RMW не так, что Foo упорядочен по 12 (я так не думаю), а то, что 12 гарантированно прибудет "после" 11 в потоке №3. Но даже тогда, на слабой платформе, что гарантирует такой заказ. Если часть магазина RMW не может выполнять спекулятивное исполнение, как это повлияет на порядок между 11 и 12?   -  person LWimsey    schedule 01.09.2017
comment
Я думаю, что в вашем первом блоке кода (с while(load), затем store) store(12) может стать глобально видимым первым (до того, как хранилище, сделавшее условие цикла ложным), а store(11) мог наступить на него. например Прогнозирование ветвления предсказывает, что цикл вращения заканчивается, хранилище запускается, затем, в конце концов, происходит загрузка, и условие ветвления оценивается и обнаруживается, что оно пошло правильным путем. Я думаю, что x86 этого не сделает, потому что он запрещает переупорядочивание LoadStore, но слабоупорядоченные ISA могут.   -  person Peter Cordes    schedule 01.09.2017
comment
Да, согласен .. Я вижу, почему первый сломан, но как RMW это исправляет, мне непонятно   -  person LWimsey    schedule 01.09.2017
comment
Но для атомарного RMW, даже на платформе, которая обычно не имеет общего порядка для отдельных загрузок или хранилищ, атомарные RMW-операторы должны убедиться, что все другие потоки / ядра видят свой вывод после операции (из любого потока), которая произвела их ввод . Представьте, что поток 3 может видеть 12 и then 11. В системе, которая работает подобным образом, три потока, увеличивающие общий счетчик, могут потерять счет (путем увеличения значения, которое уже было увеличено). Поскольку атомарные приращения должны не терять счет, атомные элементы RMW не могут быть настолько слабо упорядоченными.   -  person Peter Cordes    schedule 01.09.2017
comment
Да, но пример счетчика работает с хранилищами RMW и загрузками RMW, что гарантирует доступ к последним в порядке модификации. Обычная загрузка не должна возвращать последнее значение. В данном случае у нас есть только магазин RMW.   -  person LWimsey    schedule 01.09.2017
comment
Каким-то образом кажется, что хранилище RMW (12) упорядочивает значение глобально с более ранним хранилищем (11). Не уверен, связана ли нагрузка с этим; он просто возвращает все, что появляется первым   -  person LWimsey    schedule 01.09.2017
comment
Вы сказали: В данном случае у нас есть только магазин RMW. Вы имели в виду cmpxchg? CAS / cmpxchg - это RMW; он должен атомарно загружать / проверять / сохранять, поэтому он эквивалентен load / inc / store в зависимости от зависимостей. RMW - это всегда груз + магазин. Не существует изолированного хранилища RMW, особенно когда мы говорим об атомных RMW. Загрузка и сохранение всегда отображаются последовательно в глобальном порядке, что позволяет операции казаться атомарной для всех наблюдателей в системе. См. stackoverflow.com/questions/ 39393850 /.   -  person Peter Cordes    schedule 01.09.2017
comment
Я имел в виду часть store (12) RMW, которую видит загрузка в потоке №3. Мне было интересно, как RMW обеспечивает порядок между 11 и 12. Загрузка в № 3 возвращает то, что появляется первым.   -  person LWimsey    schedule 01.09.2017
comment
@LWimsey: (не забудьте @ ping меня. Я не получу уведомления, если вы оставите это. Вы делаете, потому что это ваш пост.) Загрузка в № 3 может никогда не увидеть 11, но если она увидит 12, это позже не сможет увидеть 11 (потому что 12 пришли из атома RMW, который имел 11 в качестве входных данных).   -  person Peter Cordes    schedule 01.09.2017
comment
@PeterCordes Моя формулировка была немного неуклюжей, но я согласен ... ветка №3 может никогда не увидеть 11, что применимо к обоим сценариям 2a и 2b. Но в случае 2a Foo становится (надежно) видимым только тогда (и если) поток № 3 загружает 11. Если он загружает 12, становится невозможным доступ к Foo, потому что он неупорядочен по 12, а 11 «потеряно» (Я назвал этот сценарий в вопросе «сломанным»)   -  person LWimsey    schedule 01.09.2017
comment
@PeterCordes В сценарии 2b RMW каким-то образом обеспечивает упорядочение, чтобы вы могли надежно получить доступ к Foo в # 3 при загрузке 11 или 12. Конечно, в обоих сценариях вращение для загрузки 11 было бы ошибкой (состояние гонки), поскольку ничто не гарантирует №3 когда-либо увидит 11, но если это так, доступ к Foo будет нормальным.   -  person LWimsey    schedule 01.09.2017
comment
Ах да, я потерял общую картину. Да, я думаю, что в 2b RMW сохраняет причинно-следственную связь, потому что он не может сделать 12 глобально видимыми до того, как 11 было. Итак, 12 означает, что Foo готов. В C ++ 11 отдельный магазин не имеет этого свойства.   -  person Peter Cordes    schedule 01.09.2017
comment
В asm для реального оборудования я думаю, что обычно безопасно атомарно загружать, а затем атомарно хранить что-то, что имеет зависимость данных от нагрузки. Но прогнозирование значений для нагрузок - это теоретическая возможность, которая нарушит это так же, как и спекулятивная зависимость управления.) Правила C ++ здесь консервативны и запрещают все, кроме атомарного RMW, распространяющего зависимость.   -  person Peter Cordes    schedule 01.09.2017
comment
Слабо упорядоченные ISA имеют определенные правила о том, какие инструкции несут зависимость данных для mo_consume упорядочивание (загрузка и последующее разыменование указателя требует только барьера LoadLoad в DEC Alpha). Я предполагаю, что это также применимо к сохранению нового значения обратно в общую переменную, даже с ослабленным порядком. Но, как я уже сказал, в C ++ 11 это всегда нарушает последовательность выпуска. Модель памяти C ++ 11 так же слаба, как Alpha. Магазин consume и release может работать.   -  person Peter Cordes    schedule 01.09.2017
comment
@PeterCordes, как RMW сохраняет причинно-следственную связь, почему бы вам не добавить это в ответ   -  person LWimsey    schedule 01.09.2017
comment
Ага, просто подумал, что пора выразить это в ответ, теперь, когда мы выяснили, какая именно часть вас интересует.   -  person Peter Cordes    schedule 01.09.2017
comment
BeeOnRope прекрасно это написал :) Вы уверены, что Foo безопасно не быть атомарным типом? В некоторых случаях переменные, отличные от atomic, не синхронизируются atomic операциями или препятствиями. например stackoverflow.com/questions/40579342/, показывает, что atomic_thread_fence не упорядочивает неатомные модели, но atomic_signal_fence делает (по крайней мере, как деталь реализации в gcc).   -  person Peter Cordes    schedule 02.09.2017
comment
@PeterCordes Да, я уверен, что Foo атомарность не нужна. Вопрос, о котором идет речь, может показаться несколько неожиданным с поведением thread_fence, но в этом случае B не является атомарным, поэтому компилятору не нужно принимать во внимание поведение между потоками. Если вы измените B на atomic<int>, это другая история, потому что тогда вы освобождаете первое хранилище до A, чтобы оно стало видимым для другого потока.   -  person LWimsey    schedule 02.09.2017
comment
Спасибо. Я знаю, что вопрос здесь неправильный, но тот факт, что signal_fence работает, а thread_fence не заставил меня задуматься, не упустил ли я что-то. Это кое-что проясняет (хотя мне, вероятно, следует опубликовать отдельный вопрос о том, является ли signal_fence запрет на неатомарные операции в gcc деталью реализации или необходимостью.)   -  person Peter Cordes    schedule 02.09.2017
comment
@PeterCordes Я все равно хочу еще немного подумать, но давайте перейдем к этому вопросу.   -  person LWimsey    schedule 02.09.2017
comment
@PeterCordes Относительно ваших рассуждений о том, почему первый случай может потерпеть неудачу: ... предсказание ветвления предсказывает, что цикл вращения заканчивается, хранилище запускается, затем, в конечном итоге, происходит загрузка, и условие ветвления оценивается и обнаруживается, что оно пошло правильным путем. .., но до того, как условие перехода станет истинным, инструкция сохранения (sync=12) еще не удалена, поэтому она еще не видна глобально, и в момент, когда условие перехода становится истинным, хранилище sync=11 уже (глобально?) видно, и так же fp = new Foo, поэтому кажется, что поток 3 получает ненулевой указатель даже в этом случае.   -  person undermind    schedule 01.01.2019
comment
@PeterCordes даже на платформе, которая обычно не имеет общего заказа для отдельных грузов или магазинов, что это за платформа?   -  person curiousguy    schedule 01.07.2019
comment
@curiousguy: Например, модели памяти ARM и PowerPC позволяют это. Существует реальное оборудование PowerPC, которое может привести к тому, что 2 читателя не согласятся с порядком хранения 2, выполняемого двумя другими потоками. Будут ли две атомарные записи в разные места в разных потоках всегда отображаться в одном порядке другими потоками? Это невозможно на x86 (Параллельные магазины в последовательном порядке)   -  person Peter Cordes    schedule 01.07.2019
comment
@PeterCordes Итак, чтобы уточнить, в этих моделях памяти нет глобального порядка операций во всех местоположениях, но есть по одному для каждого конкретного местоположения, верно?   -  person curiousguy    schedule 01.07.2019
comment
@curiousguy: Я даже не уверен, правда ли это. Модель памяти C ++ достаточно слабая, чтобы можно было предсказывать значения (для ослабленных нагрузок), поэтому чтение может происходить до записи этого значения. Один поток может иметь правильное предсказание значения и видеть запись до того, как это произойдет, а другой поток может не иметь. Так что, вероятно, нет полного порядка чтения + записи для одного места. Но я думаю, что, вероятно, существует порядок записи + атомарные RMW, с которым все потоки могут согласиться для любого заданного атомарного объекта. (Впрочем, это не обязательно должно иметь логический смысл.) Я не уверен насчет PPC / ARM asm для этого.   -  person Peter Cordes    schedule 01.07.2019
comment
@PeterCordes Чтение значений из будущего - это безумие, и его нужно запретить, если этого не произойдет ни на одном реальном процессоре.   -  person curiousguy    schedule 01.07.2019
comment
@curiousguy: Почему? В большинстве случаев нет никакой разницы в том, что читатель задерживается по отношению к писателю. Прогнозирование значения должно быть проверено, прежде чем загрузка может быть удалена, как и прогнозирование ветвления. Прогнозирование ценности по-прежнему является в основном теоретической идеей в компьютерной архитектуре, но я не понимаю, почему вы бы запретили это для невысоких нагрузок. Кроме того, кэш-банкинг Alpha 21264 - это еще один способ нарушить причинно-следственную связь и получить то, что выглядит как загрузка из будущего перед зависимой от данных загрузкой указателя. Я совершенно забыл, как это работает, но на самом деле это HW, почему mo_consume! = Расслаблено.   -  person Peter Cordes    schedule 01.07.2019
comment
@curiousguy: если вам нужен порядок, используйте mo_acquire или mo_consume. Но в большинстве реализаций C ++ на реальном аппаратном обеспечении никакие нагрузки из будущего не могут происходить на практике; это не то, что компиляторы могут создавать во время компиляции. Но бумажная модель памяти IIRC, PowerPC, достаточно слабая, чтобы код, скомпилированный сейчас, но работающий на гипотетической / будущей PPC с прогнозированием значения, мог это сделать. Код, скомпилированный сейчас с mo_acquire, уже должен использовать достаточно барьеров, чтобы не было проблем.   -  person Peter Cordes    schedule 01.07.2019


Ответы (1)


Является ли сценарий с потоком 2b правильной последовательностью выпуска?

Да, согласно вашей цитате из стандарта.

Каковы особые свойства операции чтения-изменения-записи, обеспечивающие верность этого сценария?

Что ж, несколько круговой ответ заключается в том, что единственное важное конкретное свойство - это то, что «Стандарт C ++ так определяет это».

С практической точки зрения можно спросить, почему стандарт определяет это так. Я не думаю, что вы обнаружите, что ответ имеет глубокую теоретическую основу: я думаю, что комитет мог бы также определить его так, чтобы RMW не участвовал в последовательности выпуска, или (возможно с большим трудом) определили так, что как RMW, так и отдельные mo_relaxed загрузка и сохранение участвуют в последовательности выпуска без ущерба для «надежности» модели.

Они уже дают представление о том, почему они не выбрали второй подход:

Такое требование иногда мешает эффективной реализации.

В частности, на любой аппаратной платформе, которая позволяет переупорядочивать загрузку-хранилище, это будет означать, что даже mo_relaxed загрузки и / или хранилища могут потребовать барьеров! Такие платформы существуют и сегодня. Даже на более строго упорядоченных платформах это может препятствовать оптимизации компилятора.

Так почему же тогда они не приняли другой «последовательный» подход, не требуя, чтобы RMW mo_relaxed участвовал в последовательности выпуска? Вероятно, потому, что существующие аппаратные реализации операций RMW предоставляют такие гарантии, а характер операций RMW делает вероятным, что это будет правдой в будущем. В частности, как указывает Питер в комментариях выше, операции RMW, даже с mo_relaxed, концептуально и практически на 1 сильнее, чем отдельные загрузки и хранилища: они были бы совершенно бесполезны, если бы у них не было согласованной суммы порядок.

Как только вы примете, что именно так работает оборудование, с точки зрения производительности имеет смысл согласовать стандарт: если бы вы этого не сделали, у вас были бы люди, использующие более строгие порядки, такие как mo_acq_rel, только для получения гарантий последовательности выпуска, но на самом деле оборудование со слабо упорядоченным CAS, это не бесплатно.


1 «Практическая» часть означает, что даже самые слабые формы инструкций RMW обычно являются относительно «дорогими» операциями, занимающими десяток или более циклов на современном оборудовании, в то время как mo_relaxed загрузка и сохранение обычно просто компилируются в простые загрузки и сохраняет в целевой ISA.

person BeeOnRope    schedule 01.09.2017
comment
Циклическое рассуждение подтверждает то, что определяет стандарт, но не содержит подробностей о том, «почему». Вы говорите: Вероятно, потому что существующие аппаратные реализации операций RMW предоставляют такие гарантии .. Да, очевидно, поведение RMW настолько согласовано на разных платформах, что комитет решил включить его в стандарт. Но, вероятно, на него нет ответа, поскольку это не учебник; он не (должен) ничего объяснять. - person LWimsey; 02.09.2017
comment
Кроме того, вы имеете в виду согласованный общий порядок .. Если вы имеете в виду порядок модификации sync, то есть 10,11,12, в обоих сценариях (с RMW и без него). может немного сбивать с толку - person LWimsey; 02.09.2017