Перейти к содержимому

Volatile что это

  • автор:

Volatile что это

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

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

Объявление переменной как volatile — это директива компилятору. Компилятор — программа, которая переводит программный код C/C++ в машинный код, представляющий собой набор команд для микроконтроллера ATmega в Ардуино.

По сути, эта директива заставляет компилятор размещать переменную в ОЗУ, а не во внутренних регистрах, использующихся для временного хранения и обработки различных переменных. При определенных условиях, значение переменной, хранимой в регистрах, может быть неточным.

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

Пример:

// переключение светодиода при изменении состояния вывода int pin = 13; volatile int state = LOW; void setup() < pinMode(pin, OUTPUT); attachInterrupt(0, blink, CHANGE); >void loop() < digitalWrite(pin, state); >void blink()

volatile для «чайников»

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

Разбирая чужие исходники, часто натыкаюсь на ошибки программистов, связанные с недопониманием назначения квалификатора volatile . Результатом такого недопонимания является код, который дает редкие, совершенно непредсказуемые и, зачастую, очень разрушительные и необратимые сбои. Это особенно актуально для микроконтроллерных систем, где, во-первых, обработчики прерываний являются частью прикладного кода, а, во-вторых, регистры управления периферией отображаются в RAM общего назначения. Ошибки, связанные с неиспользованием (или неправильным использованием) квалификатора volatile трудно поддаются отладке из-за их непредсказуемости и неповторяемости. Бывает, что Си-код, в котором не предусмотрено использование volatile там, где его надо бы использовать, прекрасно работает, будучи собранным одним компилятором, и сбоит (или не работает вовсе), когда собирается другим.

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

Определение

( volatile в переводе с английского означает «нестабильный», «изменчивый»)

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

Что это значит? Известно, что одной из характеристик компиляторов, говорящих за их качество, является способность оптимизировать генерируемый объектный код. Для этого они объединяют повторяющиеся конструкции, сохраняют в регистрах общего назначения промежуточные результаты вычислений, выстраивают последовательность команд так, чтобы минимизировать долго выполняющиеся фрагменты кода (например, обращение через косвенную адресацию), и т.д. Выполняя такую оптимизацию, они немного преобразует наш код, подменяя его идентичным с точки зрения алгоритма, но более быстрым и/или компактным. Но такую подмену можно делать не всегда. Рассмотрим пример:

char a = 0; . a |= 1; a |= 2; .

С точки зрения алгоритма устанавливаются два младших разряда в переменной a. Оптимизатор может сделать подмену такого кода одним оператором:

a |= 3;

выиграв таким образом пару тактов и пару ячеек ROM. Но представим себе, что эти же действия мы выполняем не над какой-то абстрактной переменной, а над периферийным регистром:

PORTB |= 1; PORTB |= 2;

Вот в этом случае оптимизация с заменой на «PORTB |= 3» нас может не устроить! Управляя напрямую состояниями выводов контроллера, нам часто бывает важна последовательность изменения сигналов. Например, мы формируем сигналы SPI, и один вывод (PORTB.0) — это данные, а другой (PORTB.1) — синхроимпульсы. В этом случае нам нельзя изменять состояния этих выводов одновременно, т.к. при этом нельзя гарантировать, что управляемая микросхема по синхроимпульсу получит правильные данные. И уж тем более, нам бы не хотелось, чтобы оптимизации подвергся код, формирующий синхроимпульс длительностью в один такт:

PORTB |= 2; PORTB &= ~2;

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

extern volatile unsigned char PORTB @ 0x06; // В HT-PICC extern volatile near unsigned char PORTB; // В MCC18 extern volatile near unsigned int PORTB __attribute__((__sfr__)); // В MCC30 и т.д.

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

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

Ошибки, связанные с volatile

Есть три основных типа ошибок, касающихся квалификатора volatile :

неиспользование volatile там, где нужно

обычно совершается программистами, которые не знают про существование volatile , или видели, но не понимают, что это такое;

использование volatile там, где нужно, но не так, как нужно

присуща программистам, знающим, насколько важен volatile при программировании параллельных процессов или при доступе к периферийным регистрам, но не учитывающие некоторые его нюансы;

использование volatile там, где не нужно (бывает и такое)

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

Неиспользование volatile

Глобальные переменные

Самая частая ошибка, связанная с volatile , это просто неиспользование этого квалификатора там, где это нужно. Работая с микроконтроллерами, программист почти всегда сам является автором кода основной программы и кода для обработки прерываний. Причем основная программа и прерывания должны обмениваться данными, а самым распространенным способом для этого является использование глобальных переменных (будь то счетчики, привязанные к периоду переполнения таймера, или буферы для хранения входных/выходных данных, или переменные состояния, или еще что-то).

Модификация переменной в прерывании

Рассмотрим пример для PIC24:

unsigned char Counter; void __attribute__((__interrupt__, __auto_psv__)) _T1Interrupt (void) { IFS0bits.T1IF = 0; Counter++; } void wait (unsigned char Time) { Counter = 0; while (Counter  Time) continue; }

При отключенной оптимизации данный код будет работать. Но стоит включить оптимизацию, как программа начнет зависать в функции wait(). Что происходит? Компилятор, транслируя функцию wait(), не знает про то, что переменная Counter может измениться в любой момент при возникновении прерывания. Он видит только то, что мы ее обнуляем, а затем сразу сравниваем с параметром Time. Другими словами компилятор предполагает, что переменная Time всегда сравнивается с нулем, и листинг функции wait() при включенной оптимизации будет выглядеть так:

0x5B4 wait: clr.b Counter 0x5B6 cp0.b w0 ; 0x5B8 bra nz, 0x5B6 0x5BA return

(Примечание: компилятор Си единственный однобайтовый аргумент передает в функцию через регистр w0)

Что мы видим в этом коде: производится обнуление переменной Counter, а затем параметр функции Time, переданный в нее через регистр w0, сравнивается с нулем, больше не обращая внимания на истинное значение переменной Coutner, которая исправно увеличивается при каждом прерывании по таймеру. Другими словами, мы попали в вечный цикл. Как уже говорилось, дело в том, что компилятор не предполагает, что функция будет прервана каким-то кодом, который будет производить операции над переменными, участвующими в работе функций. Здесь нам на помощь и приходит квалификатор volatile :

volatile unsigned char Counter; void __attribute__((__interrupt__)) _T1Interrupt (void) { IFS0bits.T1IF = 0; Counter++; } void wait (unsigned char Time) { Counter = 0; while (Counter  Time) continue; }

Теперь при трансляции компилятор сгенерит код, который будет каждый раз обращаться к переменной Counter:

0x5B4 wait: mov.b w0, w1 0x5B6 clr.b Counter 0x5B8 mov.b Counter, w0 ; 0x5BA sub w0, w1, [w15] 0x5BC bra nc, 0x5B8 0x5BE return
Модификация переменной в параллельной задаче

Если программа работает под управлением ОСРВ, т.е. выполнение функции может быть прервано в какой-то момент, а потом опять передано ей с того места, где она прервалась. Не важно, кооперативный ли планировщик у ОС (т.е. сам программист решает, где функции быть прерванной) или вытесняющий (тут программист вообще ничего не решает, и его функция может быть прервана абсолютно в любой момент времени более приоритетной задачей). Важно то, что компилятор не знает о том, что в середине функции может быть выполнен какой-то посторонний код. Вот фрагмент кода из одной реальной программы:

unsigned char Button; void Task_Button (char NewLevel) { unsigned int Temp; for (;;) { //. // Переключение контекста //. Temp = PORTB; //. // Обработка дребезга //. Button = 0; if (Temp & 1) Button = 1; if (Temp & 2) Button = 2; if (Temp & 4) Button = 3; } } void Task_Work () { if (!Button) PIN_RED_LED = 1; for (;;) { //. // Переключение контекста // и пр. //. switch (Button) { case 1: //. Действия по кнопке 1 break; case 2: //. Действия по кнопке 2 break; case 3: //. Действия по кнопке 3 break; } } }

Программа хорошо работала, будучи собранной компилятором HT-PICC18, но при переносе этого же кода на PIC24 (компилятор MCC30) работать перестала, т.е. совсем не реагировала на кнопки. Проблема была в том, что оптимизатор MCC30, в отличие от оптимизатора HT-PICC18, учел, что на момент выполнения switch значение переменной Button уже хранится в одном из регистров общего назначения (w0) (в PIC18 всего 1 аккумулятор, поэтому при работе с ним такое поведение менее вероятно):

; if (!Button) PIN_RED_LED = 1; mov.b Button, w0 bra nz, 1f bset.b PORTB, 0 1: . . ; switch (Button) sub.b w0, #2, [w15] bra z, Case2 sub.b w0, #3, [w15] bra z, Case3 sub.b w0, #1, [w15] bra nz, SwithEnd Case1: . Case2: . Case3: . SwithEnd:

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

После правильного объявления переменной:

volatile unsigned char Button; void Task_Button (char NewLevel) { . } void Task_Work () { . }
; if (!Button) PIN_RED_LED = 1; mov.b Button, w0 bra nz, 1f bset.b PORTB, 0 1: . . ; switch (Button) mov.b Button, w0 sub.b w0, #2, [w15] bra z, Case2 mov.b Button, w0 sub.b w0, #3, [w15] bra z, Case3 mov.b Button, w0 sub.b w0, #1, [w15] bra nz, SwithEnd Case1: . Case2: . Case3: . SwithEnd:

Локальные переменные

Часто для создания небольших задержек пользуются такими функциями:

void Delay (char D) { char i; for (i = 0; i  D; i++) continue; }

Однако, некоторые компиляторы видят в этих функциях бесполезный код и вообще не включают его в результирующий объектный код. Если эта задержка применялась для снижения скорости программного i2c (или SPI) под компилятором, например, HT-PICC, то при переносе на ядро AVR с компилятором WinAVR программа перестанет работать, вернее, она будет работать так быстро, что управляемая микросхема не будет успевать обрабатывать сигналы из-за того, что все вызовы функции Delay будут упразднены.

Чтобы этого не происходило, нужно использовать квалификатор volatile :

void Delay (char D) { volatile char i; for (i = 0; i  D; i++) continue; }

Указатели на volatile

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

unsigned int *Port; void SPI_Send (unsigned char Data) = 1; // Set data bit else *Port &= ~1; *Port  while (--i); } void main (void) { //. Port = &PORTB; //. SPI_Send(0x55); SPI_Send(0x66); //. }

Произошла та же ситуация, что и в предыдущем примере: под одним компилятором (MCC18) код работал, а под другим (MCC30) — перестал. Причина крылась в неправильно объявленном указателе. Дизассемблер функции SPI_Send() выглядел так:

0050C SPI_Send: mov.w port,w1 0050E mov.b #0x8,w2 ; i = 8 00510 While: cp0.b w0,#0 ; if (Data & 0x80) 00512 bra ges, 0x000518 00514 bset [w1],#0 ; *Port |= 1 00516 bra 0x00051a 00518 bclr [w1],#0 ; *Port &= ~1 ; *Port |= 2 0051A add.b w0,w0,w0 ; Data 0051C bclr [w1],#1 ; *Port &= ~2 0051E dec.b w2,w2 ; while (--n) 00520 bra nz, 0x000510 00522 return

Обратим внимание на то, что компилятор «выкинул» установку бита 1 («*Port |= 2»), посчитав ее лишней, т.к. почти сразу же за ней этот бит снова обнуляется, т.е. он выполнил оптимизацию без учета особенностей регистра, на который указывала переменная Port, а сделал он так потому, что программист не объяснил компилятору, что регистр непростой. Для исправления ошибки нужно было объявить переменную Port как указатель на volatile переменную:

volatile unsigned int *Port; void SPI_Send (unsigned char Data) { . }

Теперь компилятор знает, что оптимизацию над переменной, на которую указывает Port, производить нельзя. И новый листинг это отражает:

0050C SPI_Send: mov.w port,w1 0050E mov.b #0x8,w2 ; i = 8 00510 While: cp0.b w0 ; if (Data & 0x80) 00512 bra ges, 0x000518 00514 bset [w1],#0 ; *Port |= 1 00516 bra 0x00051a 00518 bclr [w1],#0 ; *Port &= ~1 0051A bset [w1],#1 ; *Port |= 2 0051C add.b w0,w0,w0 ; Data 0051E bclr [w1],#1 ; *Port &= ~2 00520 dec.b w2,w2 ; while (--n) 00522 bra nz, 0x000510 00524 return

Аргумент функции

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

void SPI_Send (unsigned char Data, volatile unsigned int *Port) { . } void main (void) { //. SPI_Send(0x55, &PORTB); SPI_Send(0x66, &PORTB); //. }

В противном случае мы получим все те же ошибки, что и в предыдущем примере.

Неправильное использование volatile

volatile != атомарность

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

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

Рассмотрим пример обращения к переменной, занимающей более чем одну ячейку памяти (для 16-разрядных контроллеров это int32, int64, float, double; для 8-разрядных — еще и int16). Я часто приводил этот пример, приведу еще раз (для HT-PICC18):

volatile unsigned int ADCValue; // Результат чтения АЦП (производится в прерывании) void interrupt isr (void)  if (ADIF && ADIE) { ADIF = 0; ADCValue = (ADRESH  8)  } void main () { //. while (1) { //. if (ADCValue  100) Alarm(); // Сравнение напряжения с пороговыми if (ADCValue > 900) Alarm(); // значениями и вызов тревоги //. } }

Обратим внимание, что переменная ADCValue имеет размерность 2 байта (для хранения 10-битного результата АЦП). Чем опасен данный код? Рассмотрим листинг одного из сравнений (допустим, первого):

; if (ADCValue < 100) Alarm();MOVLW 0 SUBWF ADCValue + 1, W MOVLW 100 BTFSC STATUS, Z SUBWF ADCValue, W BTFSS STATUS, C RCALL Alarm

Допустим, значение напряжения на входе АЦП такое, что результат преобразования равен 255 (0x0FF). И последнее значение переменной ADCValue, соответственно, тоже = 0x0FF. С этим значением начинает выполняться код сравнения со значением 100 (0x064). Сначала сравниваются старшие байты переменной и константы (0x00 с 0x00), а затем — младшие (0x64 и 0xFF). Результат, казалось бы, очевиден. Однако здесь кроется неприятность. Хоть результат АЦ-преобразования и равен 0xFF, на него влияют несколько факторов: стабильность напряжения питания (или опорного напряжения), стабильность входного сигнала, близость уровня измеряемого напряжения к порогу смены единицы младшего разряда, наводки, шумы и пр. Поэтому результат АЦ-преобразования имеет некий джиттер в одну-две единицы младшего разряда. Т.е. результат АЦП может скакать между значениями 0xFF и 0x100. И если между выполнением сравнений возникнет прерывание, то может произойти следующее:

значение ADCValue = 0x0FF;
произведено сравнение старших байтов: 0x00 и 0x00;
возникло прерывание по ADIF, в котором значение переменной ADCValue обновилось на 0x100;
производится сравнение младших байтов: 0x64 и уже 0x00!
т.к. программа думает, что было сравнение 0x000 < 0x064, то она вызывает функцию Alarm.

И квалификатор volatile здесь не спасает. Здесь спасет только запрет прерываний на время выполнения сравнений.

ADIE = 0; if (ADCValue  100) { ADIE = 1; Alarm();} if (ADCValue > 900) { ADIE = 1; Alarm();} ADIE = 1; . void Alarm (void) { ADIE = 1; // Не забыть включить прерывание при выполнении условий .

Так может быть, volatile вообще не нужен? Прерывания-то все равно запрещаются? Да, прерывания запрещаются, но volatile , все-таки нужен. Зачем? Рассмотрим почти такой же код для компилятора C30:

unsigned int ADCValue; void main (void) { //. Temp = ADCValue; while (1) { //. IEC0bits.ADIE = 0; if (ADCValue  100) { IEC0bits.ADIE = 1; Alarm();} if (ADCValue > 900) { IEC0bits.ADIE = 1; Alarm();} IEC0bits.ADIE = 1; //. } }

Вот здесь-то volatile и пригодится! Обратим внимание на строчку перед вечным циклом — присваивание значения переменной ADCValue переменной Temp. А заодно посмотрим листинг:

00536 mov.w ADCValue, w1 00538 mov.w w1, Temp ; while (1) ; ; IEC0bits.AD1IE = 0; 0053A bclr.b 0x0095,#5 ; if (ADCValue 0053C mov.w #0x63,w0 0053E sub.w w1,w0,[w15] 00540 bra leu, 0x000548 ; if (ADCValue > 900) 00542 mov.w #0x384,w0 00544 sub.w w1,w0,[w15] 00546 bra leu, 0x00054c 00548 bset.b 0x0095,#5 0054A rcall Alarm ; IEC0bits.AD1IE = 1; 0054C bset.b 0x0095,#5

Как видим, внутри цикла вообще нет обращения к переменной ADCValue, а вместо этого сравнение производится с регистром W1, куда была скопирована переменная ADCValue еще перед циклом. Поэтому, как ни будет изменяться ADCValue, наша программа этого не заметит. Так что volatile в данном случае нужен обязательно, просто не следует забывать, что этот квалификатор не гарантирует нам атомарности операций над объявленной переменной.

volatile указатели

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

volatile char *p; или char volatile *p;

Обе записи идентичны, но они не делают саму переменную p volatile -переменной. Обе записи означают «переменная p является указателем на volatile char». Последствия такого определения очевидны: при изменении значения указателя в прерывании или в параллельной задаче программа этого может не заметить, т.к. будет работать с указателем через РОН.

Правильное определение выглядит так:

char * volatile p;

Если же нужен volatile -указатель на volatile -переменную, то он объявляется так:

volatile char * volatile p;

Использование volatile не к месту

Скажу два слова об этой проблеме. У некоторых однажды нарвавшихся на проблемы, связанные с отсутствием volatile там, где он должен был быть, появляется какой-то комплекс volatile -мании (или оптимизация-фобии), когда в страхе, что «а вдруг компилятор этот код выкинет», чуть ли не все переменные объявляются с квалификатором volatile . В принципе, ничего криминального в таком использовании volatile нет, код будет работать правильно, но он будет громоздкий и неповоротливый. volatile не позволит компилятору применять оптимизацию, и некоторые фрагменты, которые, будучи оптимизированы, выполнились бы за 50-100 тактов, будут выполняться за 1000-2000 тактов. То же самое касается объемов кода (в десятки раз он, конечно, не вырастет, а вот раза в два — запросто). Другими словами появится неудовлетворенность оттого, что «и контроллер быстрый, и компилятор хороший, а код все равно тормозит».

Здесь четких рекомендаций я дать не смогу. В большинстве случаев при написании программ на Си я стараюсь придерживаться следующих правил:

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

глобальные переменные, обрабатываемые двумя и более задачами при работе под многозадачной ОС, нужно объявлять как volatile ;

указатели на периферийные регистры, а также на переменные, объявленные как volatile , нужно объявлять как указатели на volatile ;

все, что не попадает под первые три правила и не связано с периферией, рекомендуется писать, абстрагируясь от железа. Тогда становится понятно, что циклы вида «for (i = 0; i<100; i++) <>;» не несут на себе никакой алгоритмической нагрузки и могут быть удалены. А если они должны быть оставлены, то переменные следует объявлять как volatile .

во всех остальных случаях volatile будет лишним.

Дополнительно

Еще несколько замечаний:

volatile можно ставить в любое место в типе

Как уже указывалось выше, следующие записи эквивалентны:

volatile static int N; static volatile int N; static int volatile N;

Лично я для наглядности volatile выношу вперед.

volatile и typedef

volatile может быть использован при определении типов по всем правилам:

typedef volatile char vchar;

Теперь все переменные типа vchar будут volatile -переменными.

volatile и структуры

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

typedef volatile struct { unsigned char Counter; unsigned char *Buffer; } T_BUFFER;

(Обращу ваше внимание на то, что здесь Buffer является volatile -указателем на не- volatile переменную. Т.е. эквивалент определения: «unsigned char * volatile Buffer», а не «volatile unsigned char *Buffer».)

А в этой — только счетчик:

typedef struct { volatile unsigned char Counter; unsigned char *Butter; } T_BUFFER;
volatile и extern

Если переменная во внешнем модуле объявлена как volatile :

volatile int Counter;

, то и в заголовочном файле она должна быть объявлена с квалификатором volatile :

extern volatile int Counter;

Виктор Тимофеев, июнь, 2010
osa@pic24.ru

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

Модификатор volatile накладывает некоторые дополнительные условия на чтение/запись переменной. Важно понять две вещи о volatile переменных:

  1. Операции чтения/записи volatile переменной являются атомарными.
  2. Результат операции записи значения в volatile переменную одним потоком, становится виден всем другим потокам, которые используют эту переменную для чтения из нее значения.

Кажется, что для человека, задающего вопрос вроде вашего, достаточно знать эти два момента.

Отслеживать
ответ дан 20 апр 2014 в 19:25
13.1k 1 1 золотой знак 28 28 серебряных знаков 28 28 бронзовых знаков

насчёт объектов: слово volatile запрещает создавать доп. ссылки в сторонних потоках ? а т.к. все эти ссылки ссылаются на один и тот же объект то нет ниченго плохого в том чтобы плодить эти ссылки , следовательно для объектным ссылок volatile можно не писать. я прав?

20 апр 2014 в 19:56

Читайте хотя бы википедию: «даже модификатор volatile переменной не означает реальной необходимости применения атомарных операций».

20 апр 2014 в 20:23
@huffman это вы мне или автору вопроса? 🙂
20 апр 2014 в 20:32

@argamidon нет, неправы. Модификатор volatile ничего не запрещает. Вообще все завсиит от задачи. Присваивание значения ссылке в JVM — атомарно. Но это не решает проблемы с ошибками видимости в других потоках. Поэтому нужен volatile, ну или AtomicRef.

20 апр 2014 в 20:37

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

21 апр 2014 в 4:32

Это означает, что значение переменной будет «всегда читаться». Например, в многопоточных приложениях один поток прочёл значение a=1, передал управление другому потоку, который изменил значение на a=2, потом управление вернулось. Так вот, без volatile значение a у первого потока будет 1, т.к. первый поток «помнит», что a=1, с volatile — 2, т.к. первый поток снова прочтет значение и получит уже измененное.

Отслеживать
ответ дан 10 янв 2011 в 16:36
1,894 11 11 серебряных знаков 13 13 бронзовых знаков
Как понять «всегда читаться»? Оно что, константное? Почему тогда просто не написать final?
10 янв 2011 в 18:09

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

10 янв 2011 в 19:37

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

20 янв 2011 в 16:22

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

20 апр 2014 в 18:34

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

20 апр 2014 в 19:29

  • у переменной есть мастер копия плюс по копии на каждую нить, что её используют. Мастер копия синхронизируется с локальной копией нити при входе/выходе в/из блока synchronized. Иногда, например, пустой блок synchronized(lock)<> имеет смысл.
  • у переменных с модификатором volatile локальных копий нет. Все нити работают с мастер копией.

Отслеживать
534 1 1 золотой знак 3 3 серебряных знака 10 10 бронзовых знаков
ответ дан 19 янв 2011 в 22:43
357 2 2 серебряных знака 5 5 бронзовых знаков
Верное, но очень сложное для новичков объяснение.
20 янв 2011 в 16:19

упрощение приводит к артефактам в коде. Ещё надо отметить, что volatile не панацея при кодировании нескольких нитей: если используются неатомарные операции, например += или ++

20 янв 2011 в 22:06
Не упрощение приводит к ним, а непонимание. Но никто не совершенен. Нельзя с места, да в карьер 🙂
21 янв 2011 в 0:21
Сильно программа замедляется, если не использовать локальную копию?
26 фев 2020 в 13:36

Вот это объяснение спасибо! Теперь скажите пожалуйста что прочитать, чтобы эти знания получить в полном ввиде, а не форума черпать?)

13 апр в 6:50

Вот какое определение дается в статье «Многопоточность Java» на сайте http://alfalavista.ru.

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

В целях повышения производительности спецификация языка Java допускает сохранение в JRE локальной копии переменной для каждого потока, который на нее ссылается. Такие «локальные» копии переменных напоминают кэш и помогают потоку избежать обращения к главной памяти каждый раз, когда требуется получить значение переменной. При запуске двух потоков один из них считывает переменную A как 5, а второй ― как 10. Если значение переменной А изменилось с 5 на 10, то первый поток не узнает об изменении и будет хранить неправильное значение A. Но если переменная А помечена как volatile , то когда бы поток не считывал значение A, он будет обращаться к главной копии A и считывать ее текущее значение. Локальный кэш потока имеет смысл в том случае, если переменные в ваших приложениях не будут изменяться извне.

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

Значение ключевого слова volatile в C

Обложка поста Значение ключевого слова volatile в C

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

Волатильную целочисленную переменную можно объявить как:

int volatile х;volatile int х;

Чтобы объявить указатель на эту переменную, нужно сделать следующее:

volatile int *х;int volatile *х;

Волатильный указатель на неволатильные данные используется редко, но допустим:
int *volatile х;

Если вы хотите объявить волатильный указатель на волатильную область памяти, необходимо сделать следующее:

int volatile *volatile х;

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

int opt = 1; void Fn(void)

На первый взгляд кажется, программа зациклится. Компилятор может оптимизировать ее следующим образом:

void Fn(void) < start: int opt = 1; if (true) goto start; )

Вот теперь цикл точно станет бесконечным. Однако внешняя операция позволит записать 0 в переменную opt и прервать цикл.

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

volatile int opt = 1; void Fn(void)

Волатильные переменные используются как глобальные переменные в многопотоковых программах — любой поток может изменить общие переменные. Мы не хотим оптимизировать эти переменные.

Разбор взят из книги Гейл Л. Макдауэлл «Cracking the Coding Interview» (есть в переводе).

Следите за новыми постами по любимым темам

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *