Требуется ли volatile для разделяемой памяти, доступ к которой осуществляется через функцию доступа? - PullRequest
18 голосов
/ 30 июня 2010

[ edit ] Для фонового чтения, и, чтобы быть понятным, я говорю об этом: Введение в ключевое слово volatile

При просмотре кода встроенных систем одной из наиболее распространенных ошибок, которые я вижу, является упущение volatile для общих данных потоков / прерываний. Однако мой вопрос заключается в том, является ли «безопасным» не использовать volatile, когда к переменной обращаются через функцию доступа или функцию-член?

Простой пример; в следующем коде ...

volatile bool flag = false ;
void ThreadA()
{
    ...
    while (!flag)
    {
        // Wait
    }
    ...
}

interrupt void InterruptB()
{
    flag = true ;
} 

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

volatile bool flag = false ;
bool ReadFlag() { return flag }
void ThreadA()
{
    ...
    while ( !ReadFlag() )
    {
        // Wait
    }
    ...
}

... flag все еще должен быть изменчивым? Я понимаю, что нет ничего плохого в том, что оно нестабильно, но я беспокоюсь о том, когда оно опущено, а упущение не обнаружено; это будет безопасно?

Приведенный выше пример тривиален; в реальном случае (и причина моего запроса) у меня есть библиотека классов, которая упаковывает ОСРВ так, что существует абстрактный класс cTask, из которого получены объекты задачи. Такие «активные» объекты обычно имеют функции-члены, которые обращаются к данным, которые могут быть изменены в контексте задачи объекта, но доступны из других контекстов; критично ли то, что такие данные объявляются волатильными?

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

Ответы [ 5 ]

12 голосов
/ 30 июня 2010

Мое чтение C99 таково, что если вы не укажете volatile, то, как и когда к переменной осуществляется доступ, зависит от реализации.Если указать спецификатор volatile, тогда код должен работать в соответствии с правилами абстрактной машины .

Соответствующими частями в стандарте являются: 6.7.3 Type qualifiers (изменчивое описание) и 5.1.2.3 Program execution(определение абстрактной машины).

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

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

PS Для C ++, вероятно, стоит проверить C89, на котором основан первый.У меня нет C89 под рукой.

5 голосов
/ 30 июня 2010

Да, это критично.
Как вы сказали, volatile предотвращает оптимизацию взлома кода в общей памяти [C++98 7.1.5p8].
Поскольку вы никогда не знаете, какую оптимизацию может выполнять данный компилятор сейчас или в будущем, вам следует явно указать, что ваша переменная является изменчивой.

1 голос
/ 30 июня 2010

В C ключевое слово volatile здесь не требуется (в общем смысле).

Из спецификации ANSI C (C89), раздел A8.2 «Спецификаторы типов»:

Не существует независимой от реализации семантики для volatile объектов.

Керниган и Ричи комментируют этот раздел (ссылаясь на const и volatile спецификаторы):

За исключением того, что он должен диагностировать явные попытки изменить const объекты, компилятор может игнорировать эти квалификаторы.

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

При этом K & R также заявляет, что:

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

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

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

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

В целом обработка volatile объектов в C зависит от реализации.Согласно спецификации ANSI C89, в них нет ничего «гарантированного».

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

1 голос
/ 30 июня 2010

Конечно, во втором примере запись / изменение переменной 'flag' опущено. Если оно никогда не записывается, нет необходимости в его нестабильности.

По основному вопросу

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

Функция может быть «активной» одновременно в нескольких потоках. Представьте, что код функции - это просто схема, которая берется потоком и выполняется. Если поток B прерывает выполнение ReadFlag в потоке A, он просто выполняет другую копию ReadFlag (с другим контекстом, например другим стеком, другим содержимым регистра). И таким образом это может испортить выполнение ReadFlag в потоке A.

0 голосов
/ 30 июня 2010

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

Подробнее Редактировать: ваш вариант использования RTOS может потребовать дополнительной защиты сверх энергозависимой, в некоторых случаях вам может понадобиться использовать барьеры памяти или сделать их атомарными ... Я не могу вам точно сказать, это просточто-то, что вы должны быть осторожны (я бы посоветовал посмотреть ссылку на документацию ядра Linux, яесть ниже, хотя Linux не использует volatile для такого рода вещей, очень вероятно, по уважительной причине).Часть того, когда вам нужно и не нужно volatile, очень сильно зависит от модели памяти процессора, на котором вы работаете, и часто volatile недостаточно хороша.

volatile НЕПРАВИЛЬНОТаким образом, это НЕ гарантирует, что этот код будет работать, он не предназначен для такого использования.

volatile предназначался для чтения / записи в регистры отображаемого в памяти устройства, и как таковой этого достаточно для этой цели, однако это НЕ помогает, когда вы говорите о вещах, проходящих между потоками.(В частности, компилятор все еще может изменить порядок некоторых операций чтения и записи, так же как и процессор во время его выполнения (это ДЕЙСТВИТЕЛЬНО важно, поскольку volatile не говорит ЦП делать что-то особенное (иногда это означает обход кэша,но это зависит от компилятора / процессора))

см. http://www.open -std.org / jtc1 / sc22 / wg21 / docs / paper / 2006 / n2016.html , Intel для разработчиковarticle , CERT , документация по ядру Linux

Короткая версия этих статей, volatile использовала то, что вы хотите, и BAD , и НЕПРАВИЛЬНО. Плохо, потому что это сделает ваш код медленнее, неправильно, потому что на самом деле он не делает то, что вам нужно.

На практике, на x86 ваш код будет работать правильно с volatile или без него, однакоэто будет непереносимым.

РЕДАКТИРОВАТЬ: Примечание для самостоятельного чтения кода ... это это то, что нужно делать volatile.

...