понедельник, апреля 24, 2006

Ключевое слово volatile

В C++ есть ключевые слова, окруженные некой аурой загадочности. Просто потому, что используются они редко, в специфических случаях. volatile, mutable, typename, explicit... Несмотря на редкость использования все равно интересно знать что это такое. Да и вообще как-то неприятно, когда встречаешь и не знаешь что это.

volatile мне встречалось дважды. И оба раза мне не сразу удавалось понять, что же хотел сказать автор. При первом взгляде, это ключевое слово, скорее всего, вызовет какие-то смутные ассоциации: "мммм..... это вроде что-то связанное с потоками" (здесь речь о потоках, которые "thread", а не которые "stream"). На самом деле, указывая volatile при объявлении переменной, программист просит компилятор не оптимизировать эту переменную.

Компилятор скорее всего оптимизирует код вроде такого, если переменная cancel не менялась в теле цикла.


bool cancel = false;
while( !cancel ) {
;
}

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

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

volatile bool cancel = false;
while( !cancel ) {
;
}

Каким волшебным образом? Из другого потока? Но обеспечивать доступ к переменным из разных потоков с помощью volatile не есть хорошо. Потому что переменную надо лочить при считывании и записи. Чтобы не получилось, например, такой ситуации: один поток пишет в переменную, другой в этот же момент считывает и считает наполовину старое значение, наполовину новое, короче не пойми что получится. А если ее лочить, то и volatile не нужен. Доступ к переменным может лочиться по-разному, но по тому, что я читала в форумах, где бы вы ни лочили, volatile в дополнение к локу указывать не нужно. Более того, использование volatile в данной ситуации может сказаться на производительности.(Updated 09.01.2009: это не так как минимум для gcc 3.3.6)

Чисто теоретически указания volatile при работе с потоками достаточно, если тип данных, с которым идет работа, может быть записан на данной архитектуре атомарно, в один прием. Соответственно, надо точно знать к каким именно типам это относится. Казалось бы уж что-что, так bool должен писаться в один прием. Я вычитала, что на некоторых Windows'ах это вовсе даже и не так. И атомарность присутствует только при работе с char...

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

Update 3.11.2006
volatile в исполнении Микрософт имеет Microsoft Specific пункт. А именно: атомарность операций гарантируется и, как следствие, использование в многопоточных программах приветствуется. Но код получается непортируемым, соответственно.
Я нашла этот пункт только в документации к Visual Studio 2005. Я порылась на MSDN в поисках этого пункта в других версиях Visual Studio, не нашла.

А зачем вообще тогда нужно ключевое слово volatile? Джеймс Канзе пишет, что задумывался volatile для работы с memory mapped IO (MMIO). Интересно он пишет, переведу кусок подробно

На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа туда, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 - различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).

Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:

unsigned char* pControl = 0xff24 ;
*pControl = 0 ;
*pControl = 0 ;
*pControl = 0 ;

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


Еще пример из того же обсуждения.

char getChar()
{
static unsigned char const charAvail = 0x01 ;
volatile unsigned char* pStatus = (unsigned char*)0x1234 ;
volatile char* pData = (char*)0x1230 ;

while ( (*pStatus & charAvail) == 0 ) {
}
return *pData ;
}

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

Это все не очень актуально для Windows, Unix систем, там есть свои способы работы с MMIO.

Еще volatile, вернее выражение вида volatile sig_atomic_t, может понадобиться для работы с сигналами. По сигналам я откопала большой FAQ на русском языке, про volatile sig_atomic_t там есть.

volatile используется с setjmp/longjmp, для восстановления значения переменных. Это тоже весьма редко используемые функции, и, судя по тому, что я о них читала, компиляторы почти всегда при их использовании все сами могут восстанавливать. Если вы их используете или встретили старый код с ними, то вот, зачем тут может быть volatile: Volatile type or qualifier?.

Кроме volatile переменных бывают еще и volatile функции и volatile классы. Я знаю лишь один случай их применения. Есть весьма известная и очень критикуемая статья Андрея Александреску volatile - Multithreaded Programmer's Best Friend, где он, используя volatile, смог добиться безопасной работы с потоками с проверкой на этапе компиляции.
Он использует только синтаксические свойства volatile. Связь volatile и оптимизации никак не используется. С тем же успехом он мог бы использовать и const. Но const гораздо чаще применяется по своему прямому назначению, чем volatile, поэтому использование const создало бы больше неудобств.

В начале следующей статьи Андрей суммирует критику Generic: Min and Max Redivivus. По поводу чего критика... Во-первых, он начинает статью с того, что рассказывает о том, как хорошо volatile применять при многопоточном программировании. Что, как было сказано выше, вообще говоря неверно. Потом он снимает volatile с помощью const_cast с переменной, которая реально является volatile. Несмотря на то, что скорее всего ничем плохим это не обернется, по Стандарту C++ эта ситуация приводит к undefined behavior. Но все это ничуть не умаляет значимость этой гениальной задумки. Насколько ее удобно использовать в реальном коде это вопрос, но хотя бы с теоретической точки зрения она представляет большой интерес.

Вот и все, что мне удалось найти о volatile. Пост получился не о том как лучше и удобнее применять volatile, а скорее о том, что думать, когда вы встретите volatile в чужом коде. А вообще можно всю жизнь программировать на C++, но с volatile так и не встретиться.

Ссылки по теме:
Подробные и наглядные примеры работы компилятора при указании volatile с диассемблированными примерами кода: раз и два.
comp.lang.c++.moderated: Q: infos, articles, faqs about 'volatile'
comp.lang.c++.moderated: volatile, was: memory visibility between threads - очень большое обсуждение статей Александреску.
comp.lang.c++.moderated: volatile -- what does it mean in relation to member functions?
Doug Harrison Microsoft MVP - Visual C++ об использовании volatile
comp.lang.c++.moderated: Why use volatile anyway?
comp.programming.threads FAQ

66 коммент.:

Анонимный комментирует...

Интересно то как...

Анонимный комментирует...

>>где бы вы ни лочили, volatile в дополнение к локу указывать не нужно

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

Djuffin комментирует...

Согласен с предыдущим автором.
Volatile не прихоть а суровая необходимость. Критические секции гарантируют не одновременный доступ потоков к объекту. Но компилятор ничего не знает о них и может переменную вообще не разу не прочитать, а в место вашего цикла поставить один jamp.
(И это может стать заметно только в Release'е, где будет работать -O2 )

Alena комментирует...

Интересно то как...
:-)

Помоему это неверно. volatile нужно указывать в дополнение к локу

Тут я ориентируюсь, по тому, что читала в comp.lang.c++.moderated. Там обсуждались локи в разных системах, локи разные, где что используется. И в итоге вывод такой: лок - это некая внешняя функция, которая, теоретически, могла поменять значение переменной. Поэтому переменная обязана была быть считана заново. Я пороюсь еще и ссылки укажу, где об этом говориться. Ни одной системы, где дополнительно обязательно было бы нужно указывать volatile народ так и не упомянул.

Анонимный комментирует...

Я так понимаю, что критичеcкие секции, как правило, применяются один раз в функции, а значит и переменные меняющиеся внутри секции должны считываться из памяти! Вот если в одной функции блокировка применяется дважды и дважды идет работа с переменной - тогда компилятор без volatile может и с оптимизировать :).
Очень понравился класс LockingPtr у Andrei Alexandrescu. Возражения по поводу некорректности сильно надуманны. Не знаю, как может не работать mutex, но в крайнем случае класс легко переписать на использование тех же memory barriers. Далее, то что система может куда то перекидывать данные в зависимости от определения volatile - покажите мне эту систему! Ну и для реализации более хитрых блокировок нужно просто писать более хитрые классы.

Alena комментирует...

Вот если в одной функции блокировка применяется дважды и дважды идет работа с переменной - тогда компилятор без volatile может и с оптимизировать

Вот обещанные ссылки по этому поводу: 1, 2, 3, 4. Несмотря на то, что им отвечают, что теоретически возможны системы, где без volatile не обойтись, все на этом "теоретически" и останавливается. Конкретных примеров нет.

Анонимный комментирует...

А еще вы забыли про DOS. Реальной многопоточности там не было. Но были прерывания. И все данные которые мог поменять обработчик прерывания надо было помечать как volatile.

Анонимный комментирует...

Пример?! Их есть у меня! :)

static vector < char > buffer_;
static Mutex mtx_;
static Event ev1_, ev2_;

void func1() {
mtx_.Lock();
buffer_.resize(100);
; //Здесь должна быть загрузка данных в вектор
mtx_.Unlock();
ev1_.SetEvent();
WaitForSingleObject(ev2_,INFINITE);
ev1_.ResetEvent();
mtx_.Lock();
; //Здесь должна быть выгрузка данных из вектора
mtx_.Unlock();
};
Здесь 1 поток загружает данные в вектор buffer_ для 2 потока и устанавливает событие ev1. Далее он ждет пока 2 поток обработает данные вектора и туда же запишет свой ответ. После чего идет анализ полученных данных.
В результате размер вектора меняется, и может поменяться ссылка на данные вектора! Если компилятор при первой блокировке с оптимизирует эту ссылку в регистр, то прога может легко вылететь!
Я согласен что пример корявый (я так не пишу :) ). Но в принципе он корректен. Есть много возможностей написать лучше, НО - пусть тот кто всегда писал красиво и стильно первым бросит в меня камень!
Все будет работать если исправить первую строчку на:
volatile static vector < char > buffer_;
Однако обращение к buffer_ не оптимально. Если же использовать LockingPtr, как
void func1() {
{ LockingPtr < vector < char > > lpBuf(buffer_, mtx_);
lpBuf.resize(100);
; //Здесь должна быть загрузка данных в вектор
};
ev1_.SetEvent();
WaitForSingleObject(ev2_,INFINITE);
ev1_.ResetEvent();
{ LockingPtr < vector < char > > lpBuf(buffer_, mtx_);
; //Здесь должна быть выгрузка данных из вектора
};
};
То компилятор сможет правильно оптимизировать обращение к переменной buffer_.
PS.
С моей точки LockingPtr хорош прежде всего тем, что не дает забыть про блокировку объекта. Такую ошибку легко сделать и потом обнаружить уже после сдачи программы.

Alena комментирует...

А еще вы забыли про DOS. Реальной многопоточности там не было. Но были прерывания. И все данные которые мог поменять обработчик прерывания надо было помечать как volatile.

Интересно, не знала об этом.

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

Вот это и есть отстутсвие конкретного примера. Это лишь догадки. Вот ссылка на стандарт, документацию, прояснила бы ситуацию.

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

Переведу все же то, что по этому поводу пишет Канзе.
"... только один поток в данный момент времени имеет доступ к переменной, защищенной мьютексом. И освобождение, и захват мьютекса (по крайней мере на POSIX системах) гарантирует барьеры памяти: когда процесс A освобождает мьютекс, гарантировано, что все предыдущие изменения видны глобально, и когда процесс B захватывает мьютекс, гарантированно, что он будет видеть все глобально видимые изменения, которые предшествовали захвату. (Замечу, что оба условия необходимы. То, что изменения видимы глобально не означает, что другие процессы будут использовать глобальное состояние, а не их собственное.)".

Далее по ходу обсуждения он приводит ссылку
"In Posix (IEEE Std 1003.1, Base Definitions, General Concepts, Memory
Synchronization): "The following functions synchronize memory with
respect to other threads: [...]". Both pthread_mutex_lock and
pthread_mutex_unlock are in the list. "

Если я не верю Канзе, я могу сама стандарт почитать: The Single UNIX Specification Version 3.

Судя по примеру кода речь идет о WinAPI. Я не знаю, какие гарантии дает в таких ситуациях WinAPI. Возможно такие же, возможно нет.

Анонимный комментирует...

volatile используется практически во всех Unix daemons как переменная, которую можно установить в обработчике сигнала.

Рекомендую к прочтению http://linuxdevices.com/articles/AT5980346182.html

Анонимный комментирует...

статья понравилась, но...
volatile unsigned char* pStatus = (unsigned char*)0x1234 ;
while ( *pStatus ...){...}
в Win9x и далее первые 64K отведены как раз для отлавливания "ошибок Досовской молодости", и обращение к памяти по этому адрессу вызывает exception, следовательно сколько раз отработает while с volatile и без него да ни сколько :))

Анонимный комментирует...

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

Анонимный комментирует...

Я как-то использовал volatile чтобы умерить пыл багландовского оптимизатора. Без этого модификатора, он даже в отладочном режиме имеет обыкновение выкидывать переменные сразу после последнего использования. ;-(

Анонимный комментирует...

Алёна, спасибо за статью!
Очень полезно! Как раз то, что я искал!

napa3um комментирует...

The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread. (http://msdn.microsoft.com/en-us/library/12a04hfd.aspx)

И не надо ничего более выдумывать.

Alena комментирует...

2napa3um
The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware,

Фраза, которая вызывает много вопросов. Например, могут ли объекты быть использованы таким образом, если volatile не указан.

or a concurrently executing thread.

Ну вот это самодеятельность Микрософта. До того как будет принят новый Стандарт, по крайней мере.

И не надо ничего более выдумывать.

Да собственно практически все посты по cpp, что я пишу, выдумок не содержат. В основном это переведенные обсуждения из comp.lang.c++.moderated и куски из последнего Стандарта.

napa3um комментирует...

Мой коменатрий о Ваших выдумках касался фразы "А если ее лочить, то и volatile не нужен.", и далее по тексту. Так вот, volatile не обеспечивает монопольного блокирования переменной. Потому средства синхронизации (к примеру, мютекс) для доступа к этой переменной из разных потоков не отменяются. Но эти средства синхронизации ни в коем случае не отменяют необходимость в слове volatile. Пример (утрированный):

do {
pMutex->Lock();
int iLocalVar = iSharedVar;
pMutex->Unlock();
MyFun();
} while(iLocalVar);

После оптимизации этот цикл может выполнятся вечно, т.к. реальное чтение переменной iSharedVar может "заоптимизироваться" и быть вынесено за цикл. Суть в том, что мютексы - это сущности не языка (также, как и потоки), это сущности операционной системы. Без volatile компилятор "не догадается", как ему компилировать обращения к разделяемым между потоками (файберами, прерываниями и т.п.) переменным, даже если это обращение "обёрнуто" в вызовы мютекса Lock/Unlock.

napa3um комментирует...

В общем, я лишь присоединяюсь к мнению анонимного автора самого первого комментария, и всех тех, кто к нему присоединились. Volatile - это не экзотика, а очень распространённое в реальных программах (многопоточные демоны) явление.

И в итоге вывод такой: лок - это некая внешняя функция, которая, теоретически, могла поменять значение переменной.
Как компилятор узнает в моём предыдущем примере, что мютекс и iSharedVar чем-то связаны?

Alena комментирует...

2napa3um
Так вот, volatile не обеспечивает монопольного блокирования переменной.

Угу

Потому средства синхронизации (к примеру, мютекс) для доступа к этой переменной из разных потоков не отменяются.

угу

Но эти средства синхронизации ни в коем случае не отменяют необходимость в слове volatile.

Вот это у меня вызывает сомнения. Сомнения я излагала раньше вот тут.

Анонимный комментирует...

Комментарий от "OU" ("OU-a30ua")..
"Ага.."
Тут все вроде как в основном С++ "чистые программисты" комментарии составляли.

В Embedded (т.е. в программном обеспечении для "железа" (т.е. devices или/и систем на их основе))
в некоторых случаях не обойтись без volatile.
Один из них - когда указывается переменная, которая используется как адрес (порта, устройства).
:) Сам видел как "ничто не работает" без этого "volatile". :) :( :)

Анонимный комментирует...

Добавление от того же "OU".
..и дело в тех случаях "было" совершенно не в многопоточности.

Анонимный комментирует...

..и все тот же "OU"

Вообще-то во второй половине "Исходного сообщения" Алены уже приводилась цитата одного из заруюежных developers, что более научно и наглядно, чем я описал выше про IO и др.

И еще кое-что только что попалось (особенно не вникал, но похоже на правду ;-) :) ):
http://www.kalinin.ru/programming/cpp/12_09_00.shtml

Unknown комментирует...

Добрый день. Не являюсь программистом по специальности, но программировать надо.Из блога мало что понятно, а зачем я его читаю? Блог нашелся поисковиком на "volatile". Дело в том, что в книге Numerical Recipes (Press, Teukolski, Vetterling, Flannery 2002) наткнулась на это "volatile". Привожу вам отрывок оттуда, так как вы, по всей видимости, люди интересующиеся :) . Извините, уж без перевода, так как русской терминологией не владею. Речь идет об элементарном рассчете производной функции как конечной разности. В знаменателе имеем вариацию независимой переменной h, то есть
f'(x) ~= (f(x+h) - f(x)) / h.
Теперь собственно слова авторов: "...Always choose h so that x+h and x differ by an exactly representable number. This can usually be accomplished by the program step
"temp=x+h; h=temp-x (**)".
Some optimizing compilers, and some computers whose floating point chips have higher internal accuracy than is stored externally, can foil this trick; if so, it is usually enough to declare "temp" as "volatile", or esle to call a dummy function "donothing(temp)" between the two equations (**). This forces "temp" into and out of addressable memory.

Анонимный комментирует...

Цитата:
"...Always choose h so that x+h and x differ by an exactly representable number. This can usually be accomplished by the program step
"temp=x+h; h=temp-x (**)".
Some optimizing compilers, and some computers whose floating point chips have higher internal accuracy than is stored externally, can foil this trick; if so, it is usually enough to declare "temp" as "volatile", or esle to call a dummy function "donothing(temp)" between the two equations (**). This forces "temp" into and out of addressable memory."

Насколько я понял, используя фокус наподобие "temp=x+h; h=temp-x;" автор предлагает получить h равным минимальному значению, на которое могут отличаться 2 числа, близкие к x.
Далее автор сокрушается по поводу того, что некоторые компы имеют математические сопроцессоры с высокой точностью представления чисел - с большей, чем если бы это число хранилось во внешней по отношению к процессору памяти. Дескать, с такими железками такой фокус не получится. Но обычно достаточно объявить переменную temp как volatile, или разместить некую
фиктивную (ничего не делающую, "donothing(temp)") функцию между двумя уравнениями, то этого будет достаточно, что бы переменная temp размещалась именно во внешней памяти, а не во внутренних регистрах процессора.

Теперь о том, для чего мне обычно нужен volatile.
Поскольку я иногда пишу программы для котороллера M16C/62, то приходится использовать обычный C,
поскольку компилятора C++ для него у меня нет (по моему, его и в природе нет). Нужен, когда:
1)
extern volatile int port_b; //port_b - это порт ввода-вывода, а не обычная переменная;
//Записать мы можем одно, а прочитать - другое, и тут любая
//оптимизация противопоказана:
port_b = x; //запись в порт b
x = port_b; //чтение из порта b

2)
extern volatile int port_b; //port_b - это порт ввода-вывода, а не обычная переменная;
static volatile bool port_b_can_read = false,
port_b_can_write = false;
void interrupt_read()
{ port_b_can_read = true; }

void interrupt_write()
{ port_b_can_write = true; }

void read_port_b(int *res)
{
while(!port_b_can_read)
wait();
*res = port_b;
}
void write_port_b(int data)
{
while(!port_b_can_write)
wait();
port_b = data;
}
Во втором примере volatile нужен, что бы исключить регистровую оптимизацию переменных
port_b_can_read и port_b_can_write. Иначе компилятор запихнет их в регистр и цикл будет
крутится бесконечно.

Анонимный комментирует...

Недавно столкнулся с подтверждением необходимости volatile. Компилятор gcc-3.3.6, максимальная оптимизация.

Вот кусок кода на с++:
...
*ptr = *srcPtr;
mutex.leave();
...

Если переменные не волатильные, то оптимизатор выносил присвоение за выход из мьютекса.

А вот, если интересно, ассемблерные коды этого куска.
Это без volatile:
0x0806627d f0+93: mov 0x24(%ecx),%edx
0x08066280 f0+96: mov $0x1,%eax
0x08066285 f0+101: mov %eax,0x8083524
0x0806628a f0+106: mov %edx,0x18(%ecx)
Тут f0+93 и f0+106 - копирование значений, а f0+96 и f0+101 - выход из мьютекса. Как видно собственно операция копирования происходит после выхода из мьютекса.

Это с volatile:
0x08066b7b f0+123: mov 0x24(%ecx),%edi
0x08066b7e f0+126: mov $0x1,%eax
0x08066b83 f0+131: mov %edi,0x18(%ecx)
0x08066b86 f0+134: mov %eax,0x8083524
Собственно копирование f0+131 происходит до выхода из мьютекса f0+134.

Alena комментирует...

2 Анонимный:

Недавно столкнулся с подтверждением необходимости volatile. Компилятор gcc-3.3.6, максимальная оптимизация.

А вот, если интересно, ассемблерные коды этого куска.


Да, интересно!
Много было споров по этому поводу, но смотреть ассемблерный код народ ленился.

Спасибо!

Unknown комментирует...

да уж )) налетел в gcc на это, долго не мог понять как же так, что за локальные копии глобальных переменных в цикле, уже начал было терять доверие к окружающему =) RTFM!

Alatar комментирует...

Случайно наткнулся на вашу заметку с рассуждением на тему "а зачем оно вообще нужно". Хочу привести пример, где volatile реально нужен и актулен в настоящее время. Это немного обособленная сфера, но очень распространённая в настоящее время - программирование микроконтроллеров. Здесь оптимизация без volatile может изрядно попортить нервы =) Вот, например, работа с gcc под платформу AVR
http://gremlinable.livejournal.com/6808.html
с другой стороны, кстати, под тот же AVR компилятор IAR вообще трактует volatile совершенно иначе:
Volatile storage
Data stored in a volatile storage device is not retained when the power to the device is turned off. In order to preserve data during a power-down cycle, you should store it in non-volatile storage. This should not be confused with the C keyword volatile.

Alena комментирует...

2Alatar:
с другой стороны, кстати, под тот же AVR компилятор IAR вообще трактует volatile совершенно иначе

Спасибо, интересно очень.

Дима комментирует...

Алена, привет!

Простой пример в тему.

Аппаратный счетчик CRC доступен как mem-mapped register при записи в который происходит вычисление CRC и полученное значение сохраняется в регистре. В с-коде это выглядит так.
// mem-mapped register: пише
#define CRC_BASE 0x40003000
#define CRC_DR ((volatile int*)CRC_BASE)[4]
int CRC_calc(int x)
{
CRC_DR = x;
return CRC_DR;
}
Очевидно, что без volatile работать не будет.

Unknown комментирует...

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

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

Анонимный комментирует...

"...можно всю жизнь программировать на C++, но с volatile так и не встретиться"

Это относится не только к С++. Вообще имело смысл определить круг задач и проблематик, а не технологию.

Magic комментирует...

спасибо за статью - очень подробно написано.
Я ведь правильно понимаю использование в многопоточном safe коде .net без lock - некорректно?
То есть в .net имеет смысл использовать только при столкновении с unsafe кодом?

dalerank комментирует...

Вообщето неправильно вы понимаете смысл volatile... хотя он очень простой... это указание помпилятору не оптимизировать эту переменную... например как выполнится следующий код

void f()
{
int a=5;
q(a);
b(a);
v(a);
}

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

void f()
{
int a=5;
q(5);
b(5);
v(5);
}

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

Анонимный комментирует...

Тут многие писали, что volatile обязательно использовать с разделяемыми между потоками переменными. Я хотел бы еще раз резюмировать обсуждение, и указать, что это абсолютно не верно. Как справедливо написано в посте, volatile предназначался изначально именно для отключения оптимизации при Memoy mapped IO. При работе с потоками, на сколько мне известно, не существует операций, которые можно считать атомарными на всех платформах. Значит, доступ к разделяемым данным без использования критических секций является ошибкой вне зависимости от модификатора volatile из-за гонок. Ну а критические секции гарантируют наличие барьера памяти, все данные будут сброшены из регистров. Значит, с использованием критических секций volatile при работе с потоками как таковой не нужен. Что не отменяет его остроумного применения в LockingPtr Александреску.

Анонимный комментирует...

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

Анонимный комментирует...

Отличная статья с глубоким погружением в вопрос о volatile
http://www.cs.utah.edu/~regehr/papers/emsoft08-preprint.pdf

В статье рассматривается С а не С++.
В кратце - ваше кун-фу должно быть сильнее кун-фу разработчиков компилятора ;)

ezfalc0n комментирует...

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

Например (классический):
int i = 0;
i += i++ + ++i;

даст разный результат на различных компиляторах с разными настройками оптимизации.

Хотя по Страуструпу приоритет операций вроде однозначен.
И разбор выражения выглядит так:
++i;
i += i;
i += i;
i++;

Владимир комментирует...

demosito, а можно ссылку прдоставить, которая подтверждает Вашу фразу "Ну а критические секции гарантируют наличие барьера памяти, все данные будут сброшены из регистров.". Особенно сброс регистров заинтересовал, действительно ли это так?

Marat Abrarov комментирует...

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

Что-то я сомневаюсь, что барьеры памяти, как-то связаны с хранением некоторых переменных в регистрах процессора.

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

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

Это C++ и надо "честно предупреждать" компилятор, что именно ты имел в виду, когда писал это, иначе "лихой" оптимизатор может добавить головной боли.

Дима комментирует...

to Екклесиаст комментирует...

Вы правда используете переменные для ожидания события: while(!event) ?

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

Синхронизация заключается в том числе в разграничении доступа к ресурсам:
EnterCriticalSection(cs);
do something
LeaveCriticalSection(cs);
Без поддержки со стороны OS, в вашем случае код будет выглядеть как
extern volatile var;
1: while(var == LOCKED);
2: var = LOCKED;
3: do something
4: var = !LOCKED;
Так вот, ни что не гарантирует атомарности {1&2}, без нее синхронизация не возможна.

Поправьте меня если я не прав или учите матчасть.

Екклесиаст комментирует...

Я не вижу проблем использовать для синхронизации потоков переменные объявленные как volatile. Но какие типы при этом можно использовать зависит от платформы и компилятора. Например в 32-х разрядных системах операции с 32-х разрядными числами (если они выравнены по границе слова) можно считать атомарными( для записи и чтения). В современных компьютерах не используется шина данных меньше 32-х бит, а значит запись 32-х битного слова произходит за один такт и не может быть разделена на несколько операций. Только если в памяти 32-х битная переменная не выравнена по границе слова, то она считывается и записывается за 2 приёма.
Компилятор по умолчанию (если мы не укажем ему иное) int выравнивает по границе слова, значит чтение и запись тапа int - атомарная операция. То же самое относится к типу bool. При использовании типа char у нас всегда атомарная операция при чтении или записи. И поэтому если один поток пишет в volatile char, а другой только читает - у нас нет проблемм. Если мы уверены, что volatile int выравнено по границе слова, то при записи в неё одним потоком и чтении другим - мы всегда будем получать корректный результат.
Но есть и подводные камни, например:
volatile bool res=false;
...
while( res ) Sleep(10);
Даже если другой поток изменяет значение res на true (пока выполняется код перед циклом), то цикл может и не начать работать, а программа проскочет это место, словно цика и небыло (это зависит от компилятора... ну может ещё от фазы луны :), сам недавно сталкивался с данной проблемой при написании многопоточной программы на Builder C++ 2009. Кстати если код изменить:
while( 1 ) {
if( !res ) break;
Sleep(10);
}
То всё работает без проблем.
Хотя возможно, если человек незнаком с тем как работает шина данных, кеш, как процессор считыват данные из оперативной памяти, то ему лучше ничего кроме volatile char и не использовать в многопоточном программировании без использования специальных способов защиты целостности данных (мьютексы, критические секции и подобное).

Екклесиаст комментирует...

to Дима комментирует...

Вы привели совсем другой код, естественно если для синхронизации использовать две атомарных операции (как у вас написано), а не одну - тогда можно получить крах.
Пример синхронизации:
У нас есть два потока и один должен другому слать информацию (не критично по скорости пересылки, главное чтобы потоки на этом не тормозили, а делали своё дело). Заводим переменную volatile char flag=0; и переменные, которые нужно защитить.
Делаем двухтактную передачу данных:
1) Один процесс может устанавливать flag только в 1, а другой только в 0.
2) Первый процесс может работать с защищаемыми переменными только когда flag==0 и по окончанию работы устанавливает flag=1.
3) Второй процесс может работать с защищаемыми переменными только когда flag==1 и по окончанию работы устанавливает flag=0.
При этом мы имеем 100% защиту переменных для передачи без использования объектов ядра и независимо от архитектуры.
Если вы найдёте реальную дыру в этом методе - буду рад здравым замечаниям (сразу говорю, что подобный метод не заменит например мьютексы, но в конкретном случае работает).

Дима комментирует...
Этот комментарий был удален автором.
Дима комментирует...

to Екклесиаст ..
> Вы привели совсем другой код
Ну, не знаю, не знаю. Вы свой стерли, исправили, запостили заново.

Касаемо вашего примера. Вы пытаетесь упростить ситуацию. Хорошо, давайте упрощать.
Заводим с-модуль в котором объявляем:
int flag = 0;
int get_flag() {return flag;}
void set_flag(int _flag) {flag = _flag;}
все остальные видят методы через заголовочный файл. Если не используем multifile compilation, то у компилятора не будет возможности "кэшировать" значение flag в регистрах. Таким образом все будет работать без volatile.

Далее, ваш пример работает ровно до тех пор пока flag разграничивает два потока, добавьте третий и работать не будет.

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

Екклесиаст комментирует...

to Дима комментирует...
Ну если на то идти, то практически все случаи использования volatile можно заменить геттерами и сеттерами, в этом случае мы уходим от проблемы изменчивости переменных ;) и этот пример не исключение (язык C++ тяготеет к подобному методу использования переменных, чтобы защитить их от шаловливых ручек).
Но если мы сделаем эти функции инлайновскими - нет гарантии, что компилятор их не оптимизирует непредсказуемо, а если спрячем - будут накладные расходы по вызову функций. Конечно, если расходы по вызову функции составляют множество меры ноль по отношению ко времени выполнения остального кода, то использование функций предпочтительнее.
P.S. Говоря про искажение моего кода я говорил именно про:
1: while(var == LOCKED);
2: var = LOCKED;
У меня в коде подобное отсутствовало...

Анонимный комментирует...

to Екклесиаст ..

"При этом мы имеем 100% защиту переменных для передачи без использования объектов ядра и независимо от архитектуры."

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

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

Екклесиаст комментирует...

Вы правы относительно скорости, поэтому я и делал уточнение: "не критично по скорости пересылки". Я использовал данный метод для вывода на экран сколько процентов работы выполнено (прогресс). Эта информация не столь критична по скорости (задержка даже в секунду здесь была не принципиальна, а так как постоянно был поток данных, кеш обновлялся довольно быстро и данные там не залёживались). На процессорах архитектуры x86 для синхронизации процессов можно использовать ассемблерную команду LOCK, которая блокирует шину на время выполнения одной команды, гарантируя, что при этом операция не будет прервана. При блокировке операций "чтение-модификация" мы можем гарантированно потокобезапасно например прибавить к переменной общего пользования какое-то значение (например увеличить на 1). И другие подобные операции. Причём это такие "вкусности", что это было включено в стандарт C++11 (так что это имеет непосредственное отношение к C++). Там есть такие функции, которые могут потокобезопасно увеличить значение переменной, уменьшить и подобно (то, что позволяет сделать ассемблер x86 с помощью команды LOCK). Этой командой можно реализовать и синхронизацию обращения разных потоков к общим переменных. Так например критические секции в студии используют именно команду LOCK, а не элементы ядра.
Если вы программист C++ и не хотите влазить в тонкости ассемблера, то просто берите стандарт C++11 и используйте эти особенности процессоров x86 не вдаваясь в тонкости реализации. Просто помните, что на процессорах другой архитектуры стандартные операции C++11 потокобезопасного сложения и вычитания и другие (которые для реализации используют именно ассемблерную команду LOCK) могут работать заметно дольше.

ircicq комментирует...

>"Помоему это неверно. volatile нужно указывать в дополнение к локу "

Пришел к выводу что это утверждение неверное, потому что:
Любой Lock будет в своей реализации содержать "volatile-переменную", это заставит компилятор отменить перестановку инструкций. То есть освобождение lock произойдет гарантировано после выполнения предыдйщих инструкций с исходном коде. Скорее всего приведеный в блоге пример, где это не так (mutex.leave()) содержал неправильную реализацию mutex

Анонимный комментирует...

>>Любой Lock будет в своей реализации содержать "volatile-переменную", это заставит компилятор отменить перестановку инструкций.
volatile - это просто инструкция компилятору читать из памяти значение при каждом обращении и всё. никаких других допущений в общем случае делать нельзя.

Анонимный комментирует...

Только что в gcc встретил:

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

volatile конечно все исправила :) Будьте внимательны!

Анонимный комментирует...

Из примера:
"unsigned char* pControl = 0xff24;"

Как вы в 1 байт запихиваете 0xff24, если у него предел 0xFF?

Анонимный комментирует...

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

Анонимный комментирует...

По поводу "А если ее лочить, то и volatile не нужен. Доступ к переменным может лочиться по-разному, но по тому, что я читала в форумах, где бы вы ни лочили, volatile в дополнение к локу указывать не нужно. Более того, использование volatile в данной ситуации может сказаться на производительности."

volatile не нужен по той простой причине, что любая блокировка является барьером памяти (иначе работать она не будет) и необходимость в volatile отпадает.

volatile был введен в стандарт для урегулирования проблем с MMIO, когда к примеру запись типа

*ptr = 1;
*ptr = 2;

могла быть оптимизирована и первое присваивание могло быть удалено оптимизатором, но на самом деле каждая запись отправлялась на некий device.
А в многопоточном приложении ему делать нечего. Атомарности он не гарантирует (в отличии от Visual C++), проблему с memory ordering-ом не решает.

Анонимный комментирует...

По поводу "А если ее лочить, то и volatile не нужен. Доступ к переменным может лочиться по-разному, но по тому, что я читала в форумах, где бы вы ни лочили, volatile в дополнение к локу указывать не нужно. Более того, использование volatile в данной ситуации может сказаться на производительности."

volatile не нужен по той простой причине, что любая блокировка является барьером памяти (иначе работать она не будет) и необходимость в volatile отпадает.


В WinAPI все функции семейства InterlockedXXX требуют, чтобы изменяемый параметр был volatile.

Анонимный комментирует...

В WinAPI все функции семейства InterlockedXXX требуют, чтобы изменяемый параметр был volatile.
Цитата из книги Рихтера - Виндоуз для профессионалов.

Вас, наверное, заинтересовало, а не следует ли объявить как volatile и мою переменную g_fResourcelnUse в примере со спин-блокировкой. Отвечаю: нет, потому что она передается Interlocked-функции по ссылке, а не по значению. Передача перемен ной по ссылке всегда заставляет функцию считывать ее значение из памяти, и оптимизатор никак не влияет на это.

Анонимный комментирует...

В многопочточных программах есть своя специфика использования volatile.

Unknown комментирует...

http://habrahabr.ru/company/abbyy/blog/161607/
вот зачем

Unknown комментирует...

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

Анонимный комментирует...

Еще полезное применение - отладка. Очень часто компилятор оптимизирует переменные, которые хочется посмотреть в отладчике (Eclipse gcc). С volatile такого не происходит.

Unknown комментирует...

Я столкнулся с такой ситуацией. Есть стековый объект, указатель на который будет передаваться другим потокам для работы с ним. Объект далее нигде не используется текущим потоком. Сработает ли какая-нибудь там оптимизация, может ли объект удалиться раньше, чем другие потоки отработают? Lifetime optimization? И насколько может помочь volatile?
GameState state;
copy_state (state, original_state);
threads[0].state = &state;
threads[1].state = &state;
// Все, дальше state не используется в данном потоке.
for (...)
{
send_thread_command(...);
};
WaitForMultipleObjects(...);
// А потокам он нужен вплоть до сюда

Анонимный комментирует...

Теперь банановый!
volatile function https://stackoverflow.com/questions/15283223/volatile-function

Анонимный комментирует...

Эх... а мне вот приходилось volatile использовать и без всякого "волшебного" изменения переменной.
Была у меня такая вот строка:
bufer_lenght=((uint32_t)bufread[5]<<24) + ((uint32_t)bufread[6]<<16) + ((uint32_t)bufread[7]<<8) + (uint32_t)bufread[8];

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

Екклесиаст комментирует...

Конечно volatile был сделан в первую очередь для тех переменных, которые могут меняться в прерываниях и его использование сильно привязано к архитектуре. В серьёзном проекте на C++ (не привязаннном к конкретной архитектуре и операционной системе) использование volatile не желательно. Конечно это ключевое слово подходит для устранения лишнего предупреждения во время компиляции или убедить компилятор не выкидывать определённый код, но при этом больше похоже на "костыль", чем на серьёзное программирование. Одна из проблем использования volatile - осложняется поддержка кода (особенно другими программистами, которые не в курсе для чего этот костыль).
Вывод: volatile нужно знать и не использовать (если конечно это не системное программирование с использованием прерываний процессора)!

Серш комментирует...

В сисиплюс 20 волатиле убирают, вроде бы как бы
Больше такого модификатора не будет

Екклесиаст комментирует...

Весьма разумно, использование C++20 на каком-нибудь простеньком микроконтроллере без операционной системы и с использованием прерываний напрямую - мягко говоря неактуально. Уже с C++11 появились фичи, позволяющие обойтись без прямого использования volatile практически во всех ситуациях (за исключением очень не рекомендуемых применений этого модификатора). Оставляем volatile для программирования на C и C++ ниже 11-го для непосредственного программирования прерываний в одноядерных микроконтроллерах (с ограниченными ресурсами).