Каким правилам должен следовать компилятор при работе с энергозависимыми ячейками памяти? - PullRequest
12 голосов
/ 09 ноября 2010

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

volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
 //function body
}

Ответы [ 5 ]

19 голосов
/ 09 ноября 2010

То, что вы знаете, ложно.Volatile - это , а не , используемый для синхронизации доступа к памяти между потоками, применения любых видов ограждений памяти или чего-либо подобного.Операции с volatile памятью не являются атомарными, и они не гарантированно выполняются в каком-либо определенном порядке.volatile является одним из самых неправильно понятых средств на всем языке.« Volatile практически бесполезно для многопоточного программирования. »

Для чего используется volatile, это взаимодействие с отображенным в память оборудованием, обработчиками сигналов и инструкцией машинного кода setjmp.

Он также может быть использован аналогично тому, как const, и именно так Александреску использует его в этой статье .Но не заблуждайтесь.volatile не делает ваш код магически безопасным.Используемый таким особым образом, это просто инструмент, который может помочь компилятору сообщить вам, где вы могли ошибиться.Все еще зависит от вас, чтобы исправить ваши ошибки, и volatile не играет никакой роли в исправлении этих ошибок.

РЕДАКТИРОВАТЬ: Я постараюсь немного пояснить, что я простоsaid.

Предположим, у вас есть класс с указателем на что-то, что не может изменить .Вы можете сделать указатель константным:

class MyGizmo
{ 
public:
  const Foo* foo_;
};

Что const действительно для вас здесь делает?Это ничего не делает с памятью.Это не похоже на вкладку защиты от записи на старой дискете.Сама память по-прежнему доступна для записи.Вы просто не можете писать в него через указатель foo_.Так что const - это просто способ дать компилятору еще один способ сообщить вам, когда вы можете что-то испортить.Если вы напишите этот код:

gizmo.foo_->bar_ = 42;

... компилятор не допустит этого, потому что он помечен const.Очевидно, что вы можете обойти это, используя const_cast, чтобы отбросить const -несс, но если вам нужно убедиться, что это плохая идея, тогда вам не помогут.:)

Александреску использует volatile точно так же.Он ничего не делает для того, чтобы сделать память как-то «поточно-безопасной» , каким-либо образом .Он дает компилятору еще один способ сообщить вам, когда вы, возможно, облажались.Вы помечаете вещи, которые вы сделали действительно «поточно-ориентированными» (благодаря использованию реальных объектов синхронизации, таких как мьютексы или семафоры), как volatile.Тогда компилятор не позволит вам использовать их в не volatile контексте.Выдает ошибку компилятора, о которой вам нужно подумать и исправитьВы могли бы снова обойти это, отбрасывая volatile -несс используя const_cast, но это так же, как Зло, как отбрасывание const -нес.1050 * как инструмент для написания многопоточных приложений (редактируйте :), пока вы действительно не будете знать, что вы делаете и почему.Это имеет некоторое преимущество, но не в том, как думает большинство людей, и если вы используете его неправильно, вы можете писать опасно опасные приложения.

10 голосов
/ 09 ноября 2010

Это не так четко определено, как вы, вероятно, хотите.Большая часть соответствующих стандартов C ++ 98 содержится в разделе 1.9 «Выполнение программы»:

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

Доступ к объекту, обозначенному volatile lvalue (3.10), изменение объекта, вызов функции библиотечного ввода-вывода или вызов функции, выполняющей любую из этих операций,все побочные эффекты , которые являются изменениями в состоянии среды выполнения.Оценка выражения может привести к побочным эффектам.В определенных заданных точках в последовательности выполнения, называемых точками последовательности , все побочные эффекты предыдущих оценок должны быть завершены, и никаких побочных эффектов последующих оценок не должно быть.

После выполненияфункция начинается, выражения из вызывающей функции не вычисляются до тех пор, пока не будет завершено выполнение вызываемой функции.

Когда обработка абстрактной машины прерывается при получении сигнала, значения объектов с типом, отличным от volatile sig_atomic_t не определены, и значение любого объекта, отличного от volatile sig_atomic_t, которое было изменено обработчиком, становится неопределенным.

Экземпляр каждого объекта с автоматической продолжительностью хранения (3.7.2) связан с каждой записьюв его блок.Такой объект существует и сохраняет свое последнее сохраненное значение во время выполнения блока и пока блок приостановлен (путем вызова функции или получения сигнала).

Наименьшие требования к соответствующей реализацииявляются:

  • В точках последовательности объекты volatile стабильны в том смысле, что предыдущие оценки завершены, а последующие оценки еще не выполнены.

  • При завершении программы все данные, записанные в файлы, должны быть идентичны одному из возможных результатов, которые могло бы дать выполнение программы в соответствии с абстрактной семантикой.

  • Вход и выходдинамика интерактивных устройств должна происходить таким образом, чтобы побуждающие сообщения фактически появлялись до того, как программа ожидает ввода.Интерактивное устройство определяется реализацией.

Итак, что сводится к следующему:

  • Компилятор не может оптимизироватьчитает или пишет в volatile объекты.Для простых случаев, таких как упомянутый выше, это работает так, как вы думаете.Однако в таких случаях, как

    volatile int a;
    int b;
    b = a = 42;
    

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

    a = 42; b = a;
    

    или, если это возможно, как онобычно (в отсутствие volatile) генерируется

    a = 42; b = 42;
    

    (C ++ 0x, возможно, обратился к этому вопросу, я не прочитал все это.)

  • Компилятор не может переупорядочивать операции над двумя различными volatile объектами, которые встречаются в отдельных операторах (каждая точка с запятой является точкой последовательности), но ему полностью разрешено изменять доступ к энергонезависимым объектам относительно энергозависимых.Это одна из многих причин, по которой вам не следует пытаться писать свои собственные спин-блокировки, и основная причина, по которой Джон Диблинг предупреждает вас не рассматривать volatile как панацею для многопоточного программирования.

  • Говоря о потоках, вы заметите полное отсутствие какого-либо упоминания потоков в тексте стандартов.Это потому, что C ++ 98 не имеет понятия о потоках .(C ++ 0x делает, и вполне может указать их взаимодействие с volatile, но я бы не стал предполагать, что кто-нибудь еще реализует эти правила, если бы я был вами.) Следовательно, нет гарантии того, что доступvolatile объекты из одного потока видны другому потоку.Это другая основная причина, по которой volatile не особенно полезен для многопоточного программирования.

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

  • Множество людей пытаются сделать определенные обращения к объектам с volatile семантикой, например,

    T x;
    *(volatile T *)&x = foo();
    

    Это допустимо (потому что говорит "object "обозначается как volatile lvalue", а не" object с типом volatile"), но это должно быть сделано с большой осторожностью, потому что помните, что я говорил о том, что компилятору полностью разрешено изменять порядок энергонезависимыхдоступы относительно изменчивых?Это идет , даже если это один и тот же объект (насколько я знаю в любом случае).

  • Если вы беспокоитесь о переупорядочении доступа к более чем одному изменчивому значению,вам нужно понять правила точки следования , которые являются длинными и сложными, и я не буду их здесь цитировать, потому что этот ответ уже слишком длинный, но вот хорошее объяснение, которое тольконемного упрощенный .Если вам нужно беспокоиться о различиях в правилах точек последовательности между C и C ++, вы уже где-то облажались (например, как правило, никогда не перегружайте &&).

7 голосов
/ 09 ноября 2010

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

int *i = ...;
cout << *i; // line A
// ... (some code that doesn't use i)
cout << *i; // line B

В этом случае компилятор обычно предполагает, что, поскольку значение в i не было изменено между ними, можно сохранить значение из строки A (скажем, в регистре) и выведите то же значение в B. Однако, если вы пометите i как volatile, вы сообщаете компилятору, что некоторый внешний источник мог бы изменить значение в i между строками A и B, поэтомукомпилятор должен повторно извлечь текущее значение из памяти.

4 голосов
/ 09 ноября 2010

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

Вместо этого компилятор должен извлекать значение из памяти каждый раз (учитывая подсказку Зака, я должен сказать, что "каждый раз" ограничен точками последовательности).

Также последовательность записей не может использовать регистр и записывать только окончательное значение позже: каждая запись должна быть выгружена в память.

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

1 голос
/ 09 ноября 2010

Компилятору не разрешено оптимизировать чтение чтений летучего объекта в цикле, что в противном случае он обычно делал бы (т.е. strlen ()).

Обычно используется во встроенном программировании при чтении реестра оборудования по фиксированному адресу, и это значение может неожиданно измениться. (В отличие от «нормальной» памяти, она не меняется, если не записана самой программой ...)

Это его главная цель.

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

...