Подтвердить что ты не робот

Требуется ли `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 все еще нуждается в изменчивости? Я понимаю, что нет никакого вреда в том, что он нестабилен, но я беспокоюсь о том, когда он опускается, а упущение не замечено; будет ли это безопасно?

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

Ответ 3

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

Относительно основного вопроса

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

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

Ответ 4

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

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

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

Kernighan and Ritchie комментарий к этому разделу (со ссылкой на спецификаторы const и volatile):

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

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

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

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

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

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

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

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

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

Ответ 5

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

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

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

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

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

Краткая версия этих статей, volatile используется так, как вы хотите, и BAD, и WRONG. Плохо, потому что это сделает ваш код медленнее, неправильно, потому что он фактически не делает то, что вы хотите.

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

EDIT: обратите внимание на то, что я действительно читаю код... это то, что нужно сделать volatile.