InterlockedExchange и видимость памяти

Я прочитал статью Синхронизация и Проблемы с многопроцессорностью, и у меня есть вопрос о InterlockedCompareExchange и InterlockedExchange. Речь идет собственно о последнем примере в статье. У них есть две переменные iValue и fValueHasBeenComputed, и в CacheComputedValue() они изменяют каждую из них, используя InterlockedExchange:

InterlockedExchange ((LONG*)&iValue, (LONG)ComputeValue());  // don't understand
InterlockedExchange ((LONG*)&fValueHasBeenComputed, TRUE); // understand

Я понимаю, что могу использовать InterlockedExchange для изменения iValue, но достаточно ли этого просто сделать

iValue = ComputeValue();

Так действительно ли необходимо использовать InterlockedExchange для установки iValue? Или другие потоки будут правильно видеть iValue, даже если iValue = ComputeValue();. Я имею в виду, что другие потоки будут правильно видеть iValue, потому что после него стоит InterlockedExchange.

Также имеется статья Последовательная память на основе принципов Модель для платформ Microsoft Native Code. Есть пример 3.1.1 с более или менее таким же кодом. Одна из рекомендаций Make y interlocked. Обратите внимание - не одновременно y и x.

Обновление
Просто чтобы прояснить вопрос. Дело в том, что я вижу противоречие. В примере из раздела «Проблемы синхронизации и многопроцессорности» используются два InterlockedExchange. Напротив, в примере 3.1.1 «Базовое переупорядочивание» (который, я думаю, очень похож на первый пример) Херб Саттер дает эту рекомендацию

«Сделайте y взаимосвязанным: если y заблокирован, тогда нет гонки на y, потому что он атомарно обновляется, и нет гонки на x, потому что a -> b -> d».

. В этом черновике Herb не использует две взаимосвязанные переменные (если я прав, он имеет в виду использование InterlockedExchange только для y).


person Community    schedule 07.10.2011    source источник
comment
Я бы сказал, что вы правы, нужно блокировать только доступ к fValueHasBeenComputed. Почему они это сделали, я не знаю.   -  person jpalecek    schedule 07.10.2011
comment
Да, это необходимо. InterlockedExchange обещает полный барьер памяти, поэтому вы можете быть уверены, что обе переменные видны. Но он не гарантирует, в каком порядке ожидающие записи в буфер станут видимыми. Вы все равно ошибаетесь, если сначала сбрасывается обновление fValueHasBeenComputed.   -  person Hans Passant    schedule 07.10.2011
comment
@HansPassant: Неправильно. Цитирование связанного документа: полный барьер памяти гарантирует, что операции чтения и записи памяти, которые появляются перед инструкцией барьера памяти, фиксируются в памяти перед любыми операциями чтения и записи памяти, которые появляются после инструкции барьера памяти. Фактически, барьеры предназначены для обеспечения порядка, а не для видимости.   -  person jpalecek    schedule 07.10.2011
comment
@jpalecek вы не защищены в случае, если другие инструкции происходят в другом потоке между этими двумя   -  person Oleg    schedule 07.10.2011
comment
@Oleg: OMG, от чего защищены? При чем тут инструкции из другой ветки?   -  person jpalecek    schedule 07.10.2011
comment
@jpalecek Когда-нибудь слышали об условиях гонки?   -  person Oleg    schedule 07.10.2011
comment
@Oleg: запись в хорошо выровненные участки памяти атомарна на x86. Даже без взаимоблокированного обмена iValue будет обновляться атомарно (если ваш код не сделает что-то ужасное, чтобы нарушить выравнивание). Так что инструкции в другом потоке не имеют к этому никакого отношения. Более того, даже когда обе переменные обновляются с помощью взаимосвязанных операций, между ними все равно остается интервал, когда инструкции в другом потоке будут видеть значение нервного импульса iValue и старое значение fValueHasBeenComputed. Таким образом, состояние гонки все еще существует с заблокированным.   -  person jalf    schedule 07.10.2011
comment
@ Олег: Да. Код (в любой модификации) ничего не делает против гонок между двумя вызовами CacheComputedValue(). Однако гонки между Cache... и Fetch... одинаково невозможны в обоих случаях. Спорить о InterlockedExchange (особенно если вы не используете его возвращаемое значение) из-за состояния гонки - неуместно.   -  person jpalecek    schedule 07.10.2011
comment
@jalf Вы когда-нибудь слышали о многоядерных системах?   -  person Oleg    schedule 07.10.2011
comment
@jpalecek Я считаю, что jalf еще раз объяснил это. Вы поняли это сейчас? Порядок операций гарантирован, вы не гарантируете, что операции выполняются сразу одна за другой.   -  person Oleg    schedule 07.10.2011
comment
@ Олег: да, есть. Вместо того, чтобы спрашивать, слышали ли мы о, казалось бы, бесконечном потоке несвязанных слов, возможно, вы могли бы объяснить, что именно в них вы считаете уместным.   -  person jalf    schedule 07.10.2011
comment
@Oleg: вам никогда не гарантируется, что две операции будут выполнены сразу после другой (очевидно, это зависит от того, что вы подразумеваете под немедленно). Interlocked... тоже не решает этого.   -  person jpalecek    schedule 07.10.2011
comment
Независимо от того, сколько у вас процессоров или потоков, запись LONG на x86 (с использованием определения Win32 для LONG) будет (при условии, что ваши данные хорошо выровнены) будет атомарной. И независимо от того, сколько взаимосвязанных операций вы используете, между обновлением iValue и fValueHasBeenComputed будет промежуток, поэтому возникнет потенциальное состояние гонки, если другой код предполагает, что две переменные будут обновлены как одна атомарная операция.   -  person jalf    schedule 07.10.2011
comment
@jpalecek. Хорошо, просто представьте, что другой фрагмент кода проверяет fValueHasBeenComputed и получает false, даже если iValue был обновлен. Это, очевидно, не решено InterlockedExchange, но в моем первоначальном комментарии я только хотел сказать, что Ханс Пассант был прав в этом отношении.   -  person Oleg    schedule 07.10.2011


Ответы (4)


Они сделали это, чтобы предотвратить частичное чтение / запись, если адрес iValue не выровнен с адресом, который гарантирует атомарный доступ. эта проблема может возникнуть, когда два или более физических потока пытаются записать значение одновременно или один читает, а другой пытается записать одновременно.

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

person Necrolis    schedule 07.10.2011
comment
Как этот конкретный пример iValue может быть прочитан частично измененным, даже если iValue не выровнен по адресу, который гарантирует атомарный доступ? Я имею в виду, что FetchComputedValue() читает iValue только после того, как fValueHasBeenComputed был установлен в TRUE. И в этот момент iValue необходимо полностью модифицировать, даже если его модификация не атомарная. - person ; 14.10.2011
comment
@skwllsp: хотя это может быть частично верно для x86, что произойдет, если поток прервется между проверкой и чтением? другие потоки по-прежнему могут изменять значение, поэтому, когда прерванный поток снова получает управление, он все еще может читать частичное значение. то же самое относится и к инструкциям, только то, что read2 непосредственно следует за read1, не означает, что между ними не может быть записи из отдельного HW-потока. такова опасность многоядерного / многопоточного программирования, вы можете никогда ничего не предполагать, поэтому ранний алгоритм без блокировки стал жертвой проблемы ABA - person Necrolis; 14.10.2011
comment
В примере Microdoft других потоков нет. Таким образом, никакой другой поток не может изменить две переменные. - person ; 14.10.2011
comment
Я нашел здесь обсуждение той же темы: social.msdn.microsoft.com/Forums/pl-PL/parallelcppnative/thread/. Резюме: На платформах Windows функции InterlockedXxx имеют полную семантику. Значит, нужен только один InterlockedExchange (второй). Это гарантирует, что все модификации до InterlockedExchange правильно увидят другие потоки. - person ; 14.10.2011
comment
@skwllsp: пример MS предназначен для применения в системах с двумя или более потоками выполнения. да, барьер из-за блокировки перестает переупорядочивать и сериализует все записи (как я уже упоминал в своем ответе, они используют блокировку, также известную как блокировки шины, для глобализации записи), но он по-прежнему не предотвращает прерывание между двумя записями, и вам нужны оба заблокированы, чтобы сделать записи атомарными и видимыми без MFENCE, как они прямо заявили в статье в разделе «Исправление состояния гонки». В другой статье также прямо говорится, что не каждая система закрывает блокировки неявно. - person Necrolis; 14.10.2011
comment
Думаю, я не понимаю вашу точку зрения о Interlocked без MFENCE. Почему вы упоминаете без MFENCE? В моей ситуации я использую Windows на x86 / x64 и Microsoft здесь msdn.microsoft.com/en-us/library/windows/desktop/ говорит, что эта функция создает полный барьер (или ограждение) памяти, чтобы гарантировать, что операции с памятью выполняются по порядку .. - person ; 14.10.2011

Вы просто получаете атомарную операцию с InterlockedExchange. Зачем тебе это нужно? Причина InterlockedExchange делает 2 вещи.

  1. Заменяет значение переменной
  2. Возвращает старое значение

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

И вы также предотвращаете скачки данных по этому значению. здесь вы получите хорошее объяснение, почему чтение / запись на ДЛИННОМ не атомарно

person Oleg    schedule 07.10.2011
comment
Все InterlockedExchange примеры в коде OP - №1. Возвращенное значение отбрасывается - person jalf; 07.10.2011
comment
@jalf Вы смотрели ссылку? - person Oleg; 07.10.2011
comment
у вас странный способ обсуждения. Да, я просмотрел ссылку, и да, я слышал об условиях гонок, и да, я слышал о многоядерных машинах. НУ И ЧТО? Ни то, ни другое не имеет отношения к делу. На машинах x86 запись в память выровненных переменных (по крайней мере, если их длина не превышает 32 бита) является атомарной. И LONG в Win32 API имеет ширину 32 бита. Следовательно, запись объекта типа LONG по хорошо выровненному адресу будет атомарной. InterlockedExchange также дает нам доступ к старому значению, но OP не использует это. - person jalf; 07.10.2011
comment
@jalf Хм, может, я был невежлив. Простите, просто тяжелый день. Запись в память является атомарной, так что вы ни в коем случае не получите полуизмененную переменную. С другой стороны, вам не гарантируется, что ваша обновленная переменная будет немедленно синхронизирована обратно из кеша ЦП, поэтому, если у вас многоядерная система, вы можете достичь ситуации, когда ваше значение было обновлено одним ядром, но значение сохранено в кеше другого ядра (и в память) разная. Таким образом, вы должны использовать барьер памяти (забор), чтобы убедиться, что значение было сброшено перед следующим доступом, и это в основном то, что делает реализация атомарной операции. - person Oleg; 07.10.2011
comment
Олег прав. Полагаться на правильное выравнивание, размер определенного типа и поведение базового оборудования - это абсолютно ужасный код, который когда-нибудь станет чьей-то головной болью при отладке. +1 - person Carey Gregory; 07.10.2011
comment
@Carey: Невозможно писать код без зависимости от этих вещей. Для многопоточного кода очень важно полагаться на свойства базового оборудования. Если вы не предлагаете установить барьер памяти между каждой строкой кода, то есть. - person jalf; 07.10.2011
comment
@Oleg: наиболее распространенные архитектуры ЦП (включая x86) имеют согласованность кеш-памяти между ядрами, поэтому вы увидите значение, обновленное на всех ядрах, или не увидите вообще. И, глядя на код, первое значение не предназначено для использования, пока второе не будет также обновлено. Одиночный барьер памяти сбрасывает обе записи, и нет необходимости сбрасывать первую, прежде чем вторая будет обновлена. - person jalf; 07.10.2011
comment
@jalf: Я полностью не согласен. Если вы не пишете драйверы устройств, вам не следует делать никаких предположений об оборудовании, и вы, конечно же, не должны полагаться на весь набор предположений, которые могут или не могут быть верными для другой платформы, компилятора, размера элемента данных, или ОС. В многопоточном приложении операции с общими объектами, которые должны быть атомарными, требуют защиты мьютекса либо от ОС, либо от компилятора. Полагаться на особенности оборудования - очень плохая практика. - person Carey Gregory; 08.10.2011
comment
@jalf, я думаю, проблема в том, что произойдет, если ваш код будет перекомпилирован через десять лет на другой архитектуре. Если вы использовали InterlockedExchange, он все равно будет работать. В противном случае все ставки отключены. - person Harry Johnston; 08.10.2011
comment
@ Гарри, что произойдет, если ваш код будет перекомпилирован через десять лет на другой архитектуре, не проблема. Дело в том, что я вижу противоречие. В примере из раздела «Проблемы синхронизации и многопроцессорности» используются два InterlockedExchange. Напротив, в примере 3.1.1 Basic Reodering (который, я думаю, очень похож на первый пример) Herb Sutter дает эту рекомендацию Make y interlocked: Если y заблокирован, то гонки по y не происходит, потому что он обновляется атомарно. , и нет гонки на x, потому что a - ›b -› d .. В этом проекте Herb не используйте два interlocked! - person ; 08.10.2011

Есть два возможных решения наблюдаемого вами противоречия.

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

Во-вторых, дополнительная блокировка может не потребоваться в этом конкретном примере, потому что это очень особый случай: изменяется только один бит переменной. Однако в разрабатываемой спецификации это не упоминается как предпосылка, поэтому я сомневаюсь, что это сделано намеренно.

person Harry Johnston    schedule 08.10.2011

Думаю, в этом обсуждении есть ответ на вопрос: Неявные барьеры памяти.

Вопрос: вызывает ли InterlockedExchange (неявный полный забор) на T1 и T2, проверяя, что T2 будет «видеть» запись, сделанную T1 перед забором? (Переменные A, B и C), даже если эти переменные не находятся в той же строке кэша, что и Foo и Bar?

Ответ: Да - полная граница, сгенерированная InterlockedExchange, гарантирует, что записи в A, B и C не будут переупорядочены за границу, неявную в вызове InterlockedExchange. Это суть семантики барьера памяти. Они не обязательно должны находиться в одной строке кэша.

Барьеры памяти: взгляд на оборудование для программных хакеров и Замечания по программированию без блокировки для Xbox 360 и Microsoft Windows Тоже интересно.

person Community    schedule 19.10.2011