Как мне понять барьеры памяти чтения и изменчивость
некоторые языки обеспечивают volatile
модификатор, который описывается как выполнение "барьера чтения памяти" перед чтением памяти, которая поддерживает переменную.
барьер памяти чтения обычно описывается как способ убедиться, что процессор выполнил чтение, запрошенное перед барьером, прежде чем он выполнит чтение, запрошенное после барьера. Однако, используя это определение, казалось бы, что устаревшее значение все еще может быть прочитано. Другими словами, выполнение чтения в определенном порядок, по-видимому, не означает, что необходимо проконсультироваться с основной памятью или другими процессорами для обеспечения того, чтобы последующие значения, считываемые фактически, отражали последние в системе во время барьера чтения или записывались впоследствии после барьера чтения.
Итак, volatile действительно гарантирует, что современное значение считывается или просто (gasp!) что значения, которые считываются, по крайней мере, так же актуальны, как и считывания перед барьером? Или какая-то другая интерпретация? Каковы практические последствия этого ответа?
2 ответов
там читают барьеры и пишут барьеры; приобретите барьеры и барьеры отпуска. И многое другое (io vs memory и т. д.).
барьеров нет, чтобы контролировать "последнее" значение или" свежесть " значений. Они предназначены для управления относительным порядком доступа к памяти.
барьеры записи контролируют порядок записи. Поскольку запись в память выполняется медленно (по сравнению со скоростью процессора), обычно существует очередь запросов на запись, в которой записи размещаются раньше они действительно произойдут. Хотя они стоят в очереди по порядку, в то время как внутри очереди записи могут быть переупорядочены. (Так что, возможно, "очередь" - не лучшее имя...) Если вы не используете барьеры записи для предотвращения переупорядочения.
барьеры чтения контролируют порядок чтения. Из-за спекулятивного выполнения (CPU смотрит вперед и загружается из памяти раньше) и из-за существования буфера записи (CPU будет считывать значение из буфера записи вместо памяти, если он есть-т. е. CPU думает, что он просто написал X = 5, тогда зачем читать его обратно, просто убедитесь, что он все еще ждет стать 5 в буфере записи) чтение может произойти из строя.
Это верно независимо от того, что компилятор пытается сделать относительно порядка сгенерированного кода. ie "volatile" в C++ здесь не поможет, потому что он только говорит компилятору выводить код для повторного чтения значения из "памяти", он не говорит процессору, как / где его читать (т. е. "память" - это много вещей в уровень процессора).
таким образом, барьеры чтения/записи ставят блоки, чтобы предотвратить переупорядочивание в очередях чтения/записи (чтение обычно не так много очереди, но эффекты переупорядочения одинаковы).
какие блоки? - приобретение и / или освобождение блоков.
Acquire-eg read-acquire(x) добавит чтение x в очередь чтения и очистить очередь (на самом деле не смыть очередь, но добавить маркер, говорящий, что ничего не переупорядочивать перед этим читать, это как если бы очередь была сброшена). Таким образом, позже (в порядке кода) чтения могут быть переупорядочены, но не до чтения x.
Release-eg write-release (x, 5) сначала очистит (или отметит) очередь, а затем добавит запрос на запись в очередь записи. Таким образом, более ранние записи не будут переупорядочены после x = 5, но обратите внимание, что более поздние записи могут быть переупорядочены до x = 5.
обратите внимание, что я связал чтение с приобретением и запись с выпуском, потому что это типично, но отличается возможны комбинации.
приобретение и освобождение считаются "полу-барьерами" или "полу-заборами", потому что они останавливают переупорядочение только в одну сторону.
полный барьер (или полный забор) применяется как для приобретения, так и для выпуска - т. е. без переупорядочения.
обычно для программирования lockfree, или 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, действительно last, иначе другие потоки могут увидеть foo->ready early и получить неправильные значения x/y/z. Поэтому мы используем write_release
on foo - >ready, который, как упоминалось выше, эффективно "сбрасывает" очередь записи (обеспечивая фиксацию x,y,z), затем добавляет ready=true запрос в очередь. А затем добавляет запрос bar=13. Заметьте, что так как мы просто используется барьер выпуска (не полный) бар=13 может быть записан до готовности. Но нам все равно! т. е. мы предполагаем, что bar не изменяет общие данные.
теперь threadB () должен знать, что когда мы говорим "готов", мы действительно имеем в виду готов. Поэтому мы делаем read_acquire(foo->ready)
. Это чтение добавляется в очередь чтения, затем очередь сбрасывается. Обратите внимание, что w = some_global
также может быть в очереди. Так что foo - > ready можно прочитать до some_global
. Но опять же, нам все равно, так как это не является частью важные данные, с которыми мы так осторожны.
То, о чем мы заботимся,-это foo - >x/y/z. Таким образом, они добавляются в очередь чтения после получения flush/marker, гарантируя, что они читаются только после чтения foo->ready.
обратите внимание также, что это, как правило, те же самые барьеры, используемые для блокировки и разблокировки мьютекса/CriticalSection/etc. (т. е. приобрести на замок (), отпустить на разблокировку ()).
и
Я уверен, что это (т. е. acquire/ release)-это именно то, что, по словам MS docs, происходит для чтения/записи "изменчивых" переменных в C# (и, возможно, для MS C++, но это нестандартно). См.http://msdn.microsoft.com/en-us/library/aa645755 (VS.71).aspx в том числе "изменчивое чтение имеет" семантику"; то есть оно гарантированно происходит до любых ссылок на память, которые происходят после него..."
Я думаю java то же самое, хотя я не так знаком. Я подозреваю, что это точно так же, потому что вам просто не нужно больше гарантий, чем чтение-приобретение/запись-выпуск.
в вашем вопросе Вы были на правильном пути, думая, что это действительно все об относительном порядке - у вас просто были порядки назад (т. е. "значения, которые читаются, по крайней мере, так же актуальны, как чтения перед барьером? - нет, считывания до барьера не имеют значения, его считывания после барьера, которые гарантированно будут после, наоборот для пишет).
и обратите внимание, как уже упоминалось, переупорядочивание происходит как при чтении, так и при записи, поэтому только использование барьера на одном потоке, а не на другом не будет работать. ie для записи-выпуска недостаточно без чтения-приобретения. т. е. даже если вы пишете это в правильном порядке, он может быть прочитан в неправильном порядке, если вы не читали барьеры, чтобы пойти с барьерами.
и, наконец, обратите внимание, что программирование без блокировки и процессор архитектуры памяти могут быть на самом деле намного сложнее, чем это, но придерживаться acquire/release поможет вам довольно далеко.
volatile
в большинстве языков программирования не подразумевает реального барьера чтения памяти CPU, но приказ компилятору не оптимизировать чтение через кэширование в регистре. Это означает, что процесс чтения/поток получит значение "в конечном итоге". Общий метод-объявить логическое volatile
флаг, который будет установлен в обработчике сигнала и проверен в основном цикле программы.
В отличие от этого барьеры памяти CPU напрямую предоставляются либо через инструкции CPU, либо подразумеваются с помощью некоторые ассемблерные мнемоники (такие как lock
префикс в x86) и используются, например, при разговоре с аппаратными устройствами, где порядок чтения и записи в сопоставленные с памятью регистры ввода-вывода важен или синхронизирует доступ к памяти в многопроцессорной среде.
Чтобы ответить на ваш вопрос-нет, барьер памяти не гарантирует "последнее" значение, но гарантирует ордер операций доступа к памяти. Это имеет решающее значение, например, в lock-free программирование.
здесь является одним из праймеров на барьерах памяти процессора.