volatile
C/C 中的 volatile
关键字和 const
对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 "The C Programming Language" 对 volatile 修饰词的说明:
A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.
volatile
关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。- 应对场景:遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化。这是因为
volatile
提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile
关键字,则编译器可能优化读取和存储,就极有可能暂时使用寄存器中的值,此时这个变量由别的线程更新了的话,将出现不一致的现象 - 示例:
int volatile vInt
; 当要求使用volatile
声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:
volatile的功能
- (1)volatile可理解为“编译器警告指示字”
- (2)volatile告诉编译器必须每次去内存中取变量值
- (3)volatile主要修饰可能被多个线程访问的变量
- (4)volatile也可以修饰可能被未知因数更改的变量
const volatile int i=0;
const volatile int i=0;
这是一个有趣的语句:
- 首先
const
修改i
,其被定义为一个常量,不能更改,只能初始化( “const”含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”。) volatile
也修饰了i
,告诉编译器变量极有可能被未知因素更改,每次访问读值都有去内存取值( “volatile”的含义是“请不要做没谱的优化,这个值可能变掉的”,而并非“你可以修改这个值”。)
这两个使用并不矛盾,所以这里的i的属性是在本程序中,i应该是只读的,不应该被修改的,但是它也可能被外部的例如中断,共享的线程通过某种方式修改(如其他线程直接调用汇编去修改),所以这里也不该被编译器优化,虽然它是只读的不该被修改的,但是它还是会改变,我们在本程序中使用的时候,还是要每次都去读它的值,这是一种“双重保险”。
因此,const
和volatile
放在一起的意义在于:
- (1)本程序段中不能对a作修改,任何修改都是非法的,或者至少是粗心,编译器应该报错,防止这种粗心;
- (2)另一个程序段则完全有可能修改,因此编译器最好不要做太激进的优化。
volatile 指针
和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念:
- 修饰由指针指向的对象、数据是
const
或volatile
的:
const char* cpch;
volatile char* vpch;
指针自身的值——一个代表地址的整数变量,是 const
或 volatile
的:
char* const pchc;
char* volatile pchv;
volatile不能保证线程安全
- 对于非原子操作,即使有
volatile
修饰,但也不能保证线程安全:int volatile a = 0; void fun() { for (int j = 0; j < 100000; j ) a; } int main() { vector<thread> vec(10); for (int i = 0; i < 10; i) { vec[i] = thread(fun); } for (auto& it : vec) it.join(); cout << a << endl; }首先,代码中定义了一个volatile
类型的整型变量a
,声明为volatile
后,说明对变量的访问是显示的,不能从编译器缓存中读取,必须从内存中读取,保证变量的正确性和可见性。
然后定义了一个函数fun()
,该函数的作用是循环100000次,每次将变量a
的值加1,这个操作是在不同的线程中进行的,因此可能会存在并发问题。
接下来,在主函数中,创建一个包含10个元素的向量vec
,每个元素代表一个线程。在循环中,通过std::thread
类的构造函数创建一个线程并将其存储在vec
向量中。每个线程执行fun()
函数,将变量a
的值增加100000次。这个操作是在10个不同的线程中进行的,因此可能会产生并发问题。
为了保证对变量a
的访问安全和正确,主线程使用join()
函数等待所有的子线程都执行完毕后再输出变量a
的值。这样可以确保在输出变量a
之前,每一个子线程都已经完成了对变量a
的修改操作,不会产生并发问题。
最后,主线程输出变量a
的值,并且程序结束。
输出:
267423
对于原子操作,volatile能够保证线程安全atomic<int> volatile a = 0;
输出
1000000
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!