Скажем, я создаю объект типа 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
Сценарий нарушен, потому что поток № 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 правильной последовательностью выпуска?
И если это так:
- Каковы особые свойства операции чтения-изменения-записи, обеспечивающие верность этого сценария?
store(11)
иcompare_exchange(11, 12)
составляют последовательность выпуска? Они удовлетворяют всем требованиям в процитированном вами абзаце. - person Anton   schedule 16.08.2017sync.store(12, mo_relaxed);
выполниться и стать глобально видимым до того, как цикл вращения фактически загрузит11
, нарушая причинно-следственную связь. Не может быть управляющей зависимости как части реализации атомарного RMW, только истинная зависимость данных от загрузки к хранилищу, поэтому она не может нарушать причинно-следственную связь таким образом (или любым другим способом из-за этого правила C ++, разрешающего атомарный RMW будет частью последовательности релизов!) - person Peter Cordes   schedule 01.09.2017Foo
упорядочен по 12 (я так не думаю), а то, что 12 гарантированно прибудет "после" 11 в потоке №3. Но даже тогда, на слабой платформе, что гарантирует такой заказ. Если часть магазина RMW не может выполнять спекулятивное исполнение, как это повлияет на порядок между 11 и 12? - person LWimsey   schedule 01.09.2017while(load)
, затемstore
)store(12)
может стать глобально видимым первым (до того, как хранилище, сделавшее условие цикла ложным), аstore(11)
мог наступить на него. например Прогнозирование ветвления предсказывает, что цикл вращения заканчивается, хранилище запускается, затем, в конце концов, происходит загрузка, и условие ветвления оценивается и обнаруживается, что оно пошло правильным путем. Я думаю, что x86 этого не сделает, потому что он запрещает переупорядочивание LoadStore, но слабоупорядоченные ISA могут. - person Peter Cordes   schedule 01.09.2017Foo
становится (надежно) видимым только тогда (и если) поток № 3 загружает 11. Если он загружает 12, становится невозможным доступ кFoo
, потому что он неупорядочен по 12, а 11 «потеряно» (Я назвал этот сценарий в вопросе «сломанным») - person LWimsey   schedule 01.09.2017Foo
в # 3 при загрузке 11 или 12. Конечно, в обоих сценариях вращение для загрузки 11 было бы ошибкой (состояние гонки), поскольку ничто не гарантирует №3 когда-либо увидит 11, но если это так, доступ кFoo
будет нормальным. - person LWimsey   schedule 01.09.2017Foo
готов. В C ++ 11 отдельный магазин не имеет этого свойства. - person Peter Cordes   schedule 01.09.2017mo_consume
упорядочивание (загрузка и последующее разыменование указателя требует только барьера LoadLoad в DEC Alpha). Я предполагаю, что это также применимо к сохранению нового значения обратно в общую переменную, даже с ослабленным порядком. Но, как я уже сказал, в C ++ 11 это всегда нарушает последовательность выпуска. Модель памяти C ++ 11 так же слаба, как Alpha. Магазинconsume
иrelease
может работать. - person Peter Cordes   schedule 01.09.2017Foo
безопасно не быть атомарным типом? В некоторых случаях переменные, отличные отatomic
, не синхронизируютсяatomic
операциями или препятствиями. например stackoverflow.com/questions/40579342/, показывает, чтоatomic_thread_fence
не упорядочивает неатомные модели, ноatomic_signal_fence
делает (по крайней мере, как деталь реализации в gcc). - person Peter Cordes   schedule 02.09.2017Foo
атомарность не нужна. Вопрос, о котором идет речь, может показаться несколько неожиданным с поведениемthread_fence
, но в этом случаеB
не является атомарным, поэтому компилятору не нужно принимать во внимание поведение между потоками. Если вы изменитеB
наatomic<int>
, это другая история, потому что тогда вы освобождаете первое хранилище доA
, чтобы оно стало видимым для другого потока. - person LWimsey   schedule 02.09.2017signal_fence
работает, аthread_fence
не заставил меня задуматься, не упустил ли я что-то. Это кое-что проясняет (хотя мне, вероятно, следует опубликовать отдельный вопрос о том, является лиsignal_fence
запрет на неатомарные операции в gcc деталью реализации или необходимостью.) - person Peter Cordes   schedule 02.09.2017sync=12
) еще не удалена, поэтому она еще не видна глобально, и в момент, когда условие перехода становится истинным, хранилищеsync=11
уже (глобально?) видно, и так жеfp = new Foo
, поэтому кажется, что поток 3 получает ненулевой указатель даже в этом случае. - person undermind   schedule 01.01.2019mo_acquire
илиmo_consume
. Но в большинстве реализаций C ++ на реальном аппаратном обеспечении никакие нагрузки из будущего не могут происходить на практике; это не то, что компиляторы могут создавать во время компиляции. Но бумажная модель памяти IIRC, PowerPC, достаточно слабая, чтобы код, скомпилированный сейчас, но работающий на гипотетической / будущей PPC с прогнозированием значения, мог это сделать. Код, скомпилированный сейчас сmo_acquire
, уже должен использовать достаточно барьеров, чтобы не было проблем. - person Peter Cordes   schedule 01.07.2019