当前位置: 首页 > 科技观察

C语言中的volatile有什么用?

时间:2023-03-19 19:46:41 科技观察

大家好,我是小风哥。学习C语言时有一个奇怪的关键字volatile。这有什么用?volatile和compiler首先看这么一段代码:intbusy=1;voidwait(){while(busy){;}}编译它,注意这里使用了O2优化:让我们仔细看看生成的程序集:wait:moveax,DWORDPTRbusy[rip].L2:testeax,eaxjne.L2retbusy:.long1其中,L2是while循环。该指令由编译器优化。可以看到,判断是否跳出循环是通过检查寄存器eax来完成的,并没有检查变量busy所在内存的真实内容。注意这里的优化对于这段代码是正确的,但是问题是如果有其他代码修改了变量busy,那么这里的优化会导致其他代码修改变量busy根本不生效,像这样:intbusy=1;//这个函数在线程A中执行voidwait(){while(busy){;}}//这个函数是在线程B中执行的thread修改了busy变量,wait函数无法跳出循环。如果对busy变量使用volatile修饰,生成的指令变成这样:wait:.L2:moveax,DWORDPTRbusy[rip]testeax,eaxjne.L2retbusy:.long1注意这里的L2time一段时间,从busy变量所在的内存中读取数据存入eax,然后进行判断,保证每次都能读取到busy变量的最新值。其实你可以把寄存器eax看成是busy所在内存的缓存。当缓存(寄存器)与内存中的数据一致时不会有问题,但是当缓存与内存中的数据不一致时(即内存已经更新但缓存中仍然保存旧数据),而程序的运行往往出乎意料。除了多线程的例子,还有一类就是signalhandler和硬件修改变量(在C语言和硬件交互时经常遇到)。如果编译器生成像文章开头那样的指令,等待线程将不会检测到信号处理程序或硬件对变量的修改。所以这里需要告诉编译器:“别耍小聪明,不要只从寄存器中读取数据,这个变量可能在别处被修改了,使用时从内存中获取最新的数据”。现在是时候简单总结一下了,volatile只是阻止了编译器试图优化变量的读操作。volatile与多线程必须注意volatile只是保证了变量的可见性,与变量的原子访问无关。这是两个完全不同的任务。假设有一个非常复杂的结构structfoo:structdata{inta;诠释乙;诠释c;...};易失性结构数据foo;voidthread1(){foo.a=1;foo.b=2;foo.c=3;...}voidthread2(){inta=foo.a;intb=foo.b;intc=foo.c;...}你只用volatile修饰变量foo只是为了保证变量被thread1修改后,我们可以在thread2中读取到最新的值,但这并不能解决多线程并发读写需要原子访问的问题到富。锁通常用于确保对变量的原子访问。在使用锁的时候,锁本身包含了提供volatile的能力,也就是保证变量的可见性,所以在使用锁的时候没有必要使用volatile。volatile与内存顺序有些同学可能会想,如果我要用volatile修饰的变量没有那么复杂,一个int就可以了,像这样:volatileintbusy=0;A线程读取busy变量,B线程更新busy变量,是否可以在A检测到busy变化时执行特定的操作?由于volatile修饰可以保证busy每次都是从内存中读取,所以应该可以这样使用。然而,计算机在概念上可能相对简单,但在工程实践中却很复杂。我们知道,因为CPU和内存的速度相差很大,所以在CPU和内存之间有一层缓存。CPU实际上并不直接读取内存。缓存的存在会使问题复杂化。限于篇幅和本文的主题,这里就不展开了。.为了优化内存读写,CPU可能会对内存读写操作进行指令重排和重排序。好像是第N+1行代码先生效,假设X的初值为0,Y的初值为1:thread1thread2X=10if(!busy)busy=0;Y=X;当线程2检测到busy为0后读取X的值,此时读取的X值可能为0。要解决这个问题,我们需要的不是volatile,不能解决重排序问题。我们需要的是内存屏障,内存屏障。内存屏障是一种机器指令,它限制处理器在屏障指令前后的内存操作,以确保不会发生重排问题。内存屏障的作用仍然可以覆盖volatile提供的功能,所以不需要volatile。如您所见,在多线程环境中我们几乎总是不使用volatile关键字。