1. Зачем использовать volatile?

Ключевое слово volatile в C/C++ аналогично const и используется для модификации переменных, обычно для установления языкового барьера памяти. Вот объяснение модификатора volatile от Бьёрна Страуструпа (BS) в «The C++ Programming Language»:

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

Ключевое слово volatile — это модификатор типа. Переменная, объявленная с ним, указывает, что она может быть изменена факторами, неизвестными компилятору, такими как операционная система, аппаратное обеспечение или другие потоки. Когда компилятор встречает переменную, объявленную с этим ключевым словом, он больше не оптимизирует код, обращающийся к этой переменной, тем самым обеспечивая стабильный доступ к специальным адресам. Синтаксис объявления:int volatile vInt;Когда запрашивается значение переменной, объявленной с volatile, система всегда заново считывает данные из памяти, где она находится, даже если предыдущая инструкция только что прочитала данные из этого места. Более того, считанные данные немедленно сохраняются. Например:

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile указывает, что i может измениться в любой момент. Каждый раз при его использовании его необходимо читать по адресу i. Поэтому ассемблерный код, сгенерированный компилятором, повторно прочитает данные по адресу i и поместит их в b. Оптимизированный подход, однако, состоит в том, что, поскольку компилятор обнаруживает, что код между двумя чтениями i не выполняет операций с i, он автоматически помещает ранее прочитанные данные в b вместо повторного чтения из i. Таким образом, если i является регистровой переменной или представляет данные порта, легко могут возникать ошибки. Поэтому volatile может гарантировать стабильный доступ к специальным адресам. Обратите внимание, что в VC 6 обычный режим отладки не выполняет оптимизацию кода, поэтому эффект этого ключевого слова нельзя увидеть. Ниже, вставляя ассемблерный код, мы проверяем влияние наличия или отсутствия ключевого слова volatile на итоговый код программы. Введите следующий код:

Пример

#include <stdio.h> void main() { int i = 10; int a = i; printf("i = %d", a); // Назначение приведённого ниже ассемблерного оператора — изменить значение i в памяти // Но не давая компилятору узнать об этом __asm { mov dword ptr [ebp-4], 20h } int b = i; printf("i = %d", b); }

Затем запустите программу в режиме Debug, и результат вывода будет следующим:

i = 10
i = 32

Затем запустите программу в режиме Release, и результат вывода будет следующим:

i = 10
i = 10

Вывод ясно показывает, что в режиме Release компилятор оптимизировал код, и во второй раз не было выведено правильное значение i. Ниже давайте добавим ключевое слово volatile в объявление i и посмотрим, что изменится:

Пример

#include <stdio.h> void main() { volatile int i = 10; int a = i; printf("i = %d", a); __asm { mov dword ptr [ebp-4], 20h } int b = i; printf("i = %d", b); }

Запустите программу соответственно в версиях Debug и Release, и вывод в обоих случаях будет:

i = 10
i = 32

Это показывает, что ключевое слово volatile сыграло свою роль. На самом деле не только подход «манипулирование стеком через встроенный ассемблер» создаёт изменения переменных, не распознаваемые компилятором. Чаще всего, когда несколько потоков одновременно обращаются к общим переменным и один поток изменяет значение переменной, как сделать изменённое значение видимым другим потокам? Вообще говоря, volatile используется в следующих местах:

  • 1) Переменные, изменяемые в процедурах обработки прерываний и используемые для обнаружения другими программами, должны быть объявлены как volatile;
  • 2) Флаги, разделяемые между задачами в многозадачной среде, должны быть объявлены как volatile;
  • 3) Регистры оборудования, отображаемые в память, обычно также нуждаются в квалификаторе volatile, потому что каждое чтение из них или запись в них может иметь разный смысл;

2. Указатели volatile

Подобно модификатору const — у const есть понятия указателя на const и const-указателя — у volatile также есть соответствующие понятия:

Модификация объекта, на который указывает указатель; данные являются const или volatile:

const char* cpch;
volatile char* vpch;

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

Собственное значение указателя — целочисленная переменная, представляющая адрес, — является const или volatile:

char* const pchc;
char* volatile pchv;

Примечание:

  • (1) Можно присвоить non-volatile int переменной volatile int, но нельзя присвоить non-volatile объект volatile-объекту.
  • (2) Помимо базовых типов, пользовательские типы также можно модифицировать квалификатором volatile.
  • (3) В C++ класс с идентификатором volatile может обращаться только к подмножеству своего интерфейса, подмножеству, контролируемому реализатором класса. Пользователи могут использовать только const_cast, чтобы получить полный доступ к интерфейсу типа. Кроме того, volatile, как и const, распространяется от класса на его члены.

3. volatile в многопоточности

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

volatile  BOOL  bStop  =  FALSE;

(1) В одном потоке:

while(  !bStop  )  {  ...  }  
bStop  =  FALSE;  
return;    

(2) В другом потоке, чтобы завершить вышеуказанный цикл потока:

bStop  =  TRUE;  
while(  bStop  );  //等待上面的线程终止,如果bStop不使用volatile申明,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。

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

...  
int  nMyCounter  =  0;  
for(;  nMyCounter<100;nMyCounter++)  
{  
...  
}  
...

В этом фрагменте кода копия nMyCounter может храниться в регистре (в цикле проверка и операции над nMyCounter всегда выполняются со значением в этом регистре), но есть другой фрагмент кода, который выполняет такую операцию:nMyCounter -= 1;В этой операции изменение nMyCounter выполняется над nMyCounter в памяти, что порождает явление: изменения nMyCounter не синхронизированы.

Исходный адрес: https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777432.html