Как понять барьеры чтения и нестабильность памяти

Некоторые языки предоставляют модификатор volatile, который описывается как выполнение «барьера чтения памяти» перед чтением памяти, которая поддерживает переменную.

Барьер памяти чтения обычно описывается как способ гарантировать, что ЦП выполнил запрошенные чтения перед барьером, прежде чем он выполнит чтение, запрошенное после барьера. Однако, используя это определение, может показаться, что устаревшее значение все еще может быть прочитано. Другими словами, выполнение чтений в определенном порядке, похоже, не означает, что необходимо консультироваться с основной памятью или другими процессорами, чтобы гарантировать, что последующие считанные значения действительно отражают последние значения в системе во время барьера чтения или записанные впоследствии после читать барьер.

Итак, действительно ли volatile гарантирует, что считывается актуальное значение, или просто (ах!), Что считываемые значения, по крайней мере, так же актуальны, как и считанные до барьера? Или какое-то другое толкование? Каковы практические последствия этого ответа?


person Jason Kresowaty    schedule 24.11.2009    source источник


Ответы (2)


Существуют барьеры для чтения и записи; приобретать барьеры и снимать барьеры. И многое другое (io vs память и т. Д.).

Нет никаких барьеров для контроля «последней» ценности или «свежести» ценностей. Они нужны для управления относительным порядком доступа к памяти.

Барьеры записи управляют порядком записи. Поскольку запись в память выполняется медленно (по сравнению со скоростью ЦП), обычно существует очередь запросов на запись, где записи отправляются до того, как они «действительно произойдут». Хотя они ставятся в очередь по порядку, внутри очереди записи могут быть переупорядочены. (Так что, возможно, «очередь» - не лучшее название ...) Если вы не используете барьеры записи для предотвращения переупорядочения.

Барьеры чтения контролируют порядок чтения. Из-за спекулятивного выполнения (ЦП смотрит вперед и загружается из памяти раньше) и из-за наличия буфера записи (ЦП будет читать значение из буфера записи, а не из памяти, если она там есть, т. Е. ЦП думает, что он просто написал X = 5, тогда зачем читать его, просто посмотрите, что он все еще ожидает стать 5 в буфере записи) чтение может происходить не по порядку.

Это верно независимо от того, что компилятор пытается сделать в отношении порядка сгенерированного кода. т.е. 'volatile' в C ++ здесь не поможет, потому что он только сообщает компилятору, что нужно вывести код для повторного чтения значения из «памяти», он НЕ сообщает процессору, как / откуда его читать (например, из «памяти»). есть много вещей на уровне процессора).

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

Какие блоки? - приобретать и / или освобождать блоки.

Получение - например, чтение-получение (x) добавит чтение x в очередь чтения и очистит очередь (на самом деле не сбрасывать очередь, но добавить маркер, говорящий, что ничего не переупорядочивайте перед этим read, как если бы очередь была сброшена). Таким образом, более поздние (в порядке кода) чтения могут быть переупорядочены, но не до чтения x.

Освобождение - например, запись-освобождение (x, 5) сначала очистит (или отметит) очередь, а затем добавит запрос на запись в очередь на запись. Таким образом, более ранние записи не будут переупорядочены после x = 5, но обратите внимание, что более поздние записи могут быть переупорядочены до x = 5.

Обратите внимание, что я объединил чтение с получением и запись с выпуском, потому что это типично, но возможны разные комбинации.

Приобретение и Освобождение считаются «полубарьерами» или «полузаборами», потому что они только останавливают переупорядочивание в одном направлении.

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

Обычно для безблокировочного программирования, C # или java «volatile» вам нужно / нужно читать-получать и писать-выпускать.

ie

void threadA()
{
   foo->x = 10;
   foo->y = 11;
   foo->z = 12;
   write_release(foo->ready, true);
   bar = 13;
}
void threadB()
{
   w = some_global;
   ready = read_acquire(foo->ready);
   if (ready)
   {
      q = w * foo->x * foo->y * foo->z;
   }
   else
       calculate_pi();
}

Итак, во-первых, это плохой способ программирования потоков. Замки были бы безопаснее. Но просто чтобы проиллюстрировать препятствия ...

После того, как threadA () завершит запись foo, ему нужно написать foo-> ready LAST, действительно последний, иначе другие потоки могут увидеть foo-> ready раньше и получить неправильные значения x / y / z. Поэтому мы используем write_release on foo-> ready, который, как упоминалось выше, эффективно «очищает» очередь записи (обеспечивая фиксацию x, y, z), а затем добавляет в очередь запрос ready = true. А затем добавляет запрос bar = 13. Обратите внимание, что, поскольку мы только что использовали барьер выпуска (не полный), bar = 13 может быть записан до готовности. Но нам все равно! т.е. мы предполагаем, что панель не изменяет общие данные.

Теперь threadB () должен знать, что когда мы говорим «готово», мы действительно имеем в виду готово. Итак, мы делаем read_acquire(foo->ready). Это чтение добавляется в очередь чтения, ЗАТЕМ очередь очищается. Обратите внимание, что w = some_global также может быть в очереди. Итак, foo-> ready можно прочитать перед some_global. Но, опять же, нас это не волнует, поскольку это не часть важных данных, к которым мы так осторожно относимся. Что нас действительно волнует, так это foo-> x / y / z. Таким образом, они добавляются в очередь чтения после сбора данных / маркера, гарантируя, что они будут прочитаны только после чтения foo-> ready.

Также обратите внимание, что обычно это те же самые барьеры, которые используются для блокировки и разблокировки мьютекса / CriticalSection / и т. Д. (т.е. получить при блокировке (), отпустить при разблокировке ()).

So,

  • Я почти уверен, что это (т.е. получение / выпуск) именно то, что говорится в документации MS, для чтения / записи «изменчивых» переменных в C # (и, возможно, для MS C ++, но это нестандартно). См. http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx, включая «Непостоянное чтение имеет« семантику получения »; то есть гарантированно произойдет до любых ссылок на память, которые происходят после него ...»

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

  • В своем вопросе вы были на правильном пути, думая, что все дело в относительном порядке - у вас просто был порядок в обратном порядке (т.е. «считываемые значения, по крайней мере, так же актуальны, как и считанные до барьера? "- нет, чтения перед барьером неважны, его чтения ПОСЛЕ барьера, которые гарантированно будут ПОСЛЕ, и наоборот для записи).

  • И обратите внимание, как уже упоминалось, переупорядочивание происходит как при чтении, так и при записи, поэтому использование барьера только в одном потоке, а не в другом, НЕ РАБОТАЕТ. т.е. без чтения-получения недостаточно одной записи-выпуска. т.е. даже если вы напишете его в правильном порядке, он может быть прочитан в неправильном порядке, если вы не использовали барьеры чтения вместе с барьерами записи.

  • И, наконец, обратите внимание, что программирование без блокировок и архитектуры памяти ЦП могут быть на самом деле намного сложнее, чем это, но придерживаясь приобретения / выпуска, вы довольно далеко уйдете.

person tony    schedule 24.11.2009
comment
Имеет ли значение, что write_release и read_acquire ссылаются на одну и ту же готовую переменную? Или вы могли бы использовать отдельные фиктивные переменные для обоих? Переданное значение кажется бессмысленным. - person Joseph Garvin; 22.03.2010
comment
Важно использовать одну и ту же переменную для потоков, которые пытаются синхронизироваться. Точно так же, как вам нужно использовать тот же мьютекс или блокировку в обычном потоке. В моем примере threadA / B мы хотим убедиться, что foo- ›x, y, z записаны до foo-› ready (в противном случае кто-то может увидеть 'ready == true' до того, как foo действительно будет готов). Что касается чтения, вы не хотите читать x, y, z до того, как он будет готов, поэтому вам нужно read_acquire на foo- ›готово, чтобы ЦП не переупорядочил x, y, z считывает до 'if ( foo- ›готов) '. Если бы ваши барьеры были на разных фиктивных переменных, у вас не было бы точки синхронизации. - person tony; 22.03.2010

volatile в большинстве языков программирования означает не реальный барьер чтения памяти ЦП, а приказ компилятору не оптимизировать чтение посредством кэширования в регистре. Это означает, что процесс / поток чтения получит значение «в конце концов». Распространенным методом является объявление логического флага volatile, который устанавливается в обработчике сигналов и проверяется в основном цикле программы.

В отличие от этого, барьеры памяти ЦП предоставляются напрямую либо с помощью инструкций ЦП, либо подразумеваются с помощью определенной мнемоники ассемблера ( такие как префикс lock в x86) и используются, например, при разговоре с аппаратными устройствами, где важен порядок чтения и записи в отображенные в память регистры ввода-вывода, или для синхронизации доступа к памяти в многопроцессорной среде.

Чтобы ответить на ваш вопрос вопрос - нет, барьер памяти не гарантирует "последнее" значение, но гарантирует порядок операций доступа к памяти. Это очень важно, например, при программировании без блокировки.

Вот один из учебных пособий по барьерам памяти ЦП.

person Nikolai Fetissov    schedule 24.11.2009
comment
Я знаю, что это имеет место во многих реализациях C и C ++. Мой вопрос наиболее актуален для платформ виртуальных машин, таких как Java и .NET. - person Jason Kresowaty; 24.11.2009
comment
Для языков на основе виртуальных машин, таких как Java и C #, вам необходимо выяснить, какова их модель памяти. - person Nikolai Fetissov; 24.11.2009
comment
Обратите внимание, что volatile недостаточно, вы должны использовать volatile sig_atomic_t для совместимого со стандартами использования в обработчике сигналов. - person Jed; 22.09.2012