Как я понимаю Барьеры чтения памяти и изменчивость

StackOverflow https://stackoverflow.com/questions/1787450

Вопрос

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

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

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

Обычно для программирования без блокировки, или 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 ПОСЛЕДНИМ, действительно последним, иначе другие потоки могут увидеть foo-> ready раньше и получить неправильные значения x / y / z.Поэтому мы используем write_release на foo->ready, который, как упоминалось выше, эффективно "очищает" очередь записи (гарантируя, что x, y, z зафиксированы), затем добавляет запрос ready= true в очередь.А затем добавляет запрос bar = 13.Обратите внимание, что, поскольку мы только что использовали барьер освобождения (не полный), bar = 13 может быть записан до готовности.Но нам все равно!т.е. мы предполагаем, что bar не изменяет общие данные.

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

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

Итак,

  • Я почти уверен, что это (т. Е. получение / выпуск) - это именно то, что, по словам MS docs, происходит для чтения / записи 'volatile' переменных в C # (и необязательно для MS C ++, но это нестандартно).Видишь http://msdn.microsoft.com/en-us/library/aa645755 (ПРОТИВ 71).aspx включая "Изменчивое чтение имеет "приобретенную семантику"";то есть, это гарантированно произойдет до любых ссылок на память, которые произойдут после этого ..."

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

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

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

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

Другие советы

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

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

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

Здесь является одним из основных средств устранения барьеров памяти процессора.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top