C语言关键词volatile的用法探析.docx
学C语言时有一个奇怪的关键字volatile,这到底有什么用呢?volatile与编译器首先来看这样一段代码:intbusy=l;voidwait()while(busy)j)编译一下,注意,这里使用02优化:AaSMo>d+AddE.。VVe.86<649cc12.1*©-021 X”"AOCr,TIUKMtes÷Addnew.w/AddtooLvI Tt:2 I Fa>f CMMO F > :3.L2:“t*tx. mW6 E7 uy:8 .1O0<123 voidMit()(4il(bu>y)让我们仔细看看生成的这段汇编:Waitzmoveax,DWORDPTRbusy|rip.L2:testeax,eaxjne.L2retbusy:.Iongl其中L2这一段即为WhiIe循环,这段指令是经过编译器优化的,可以看到,决定能否跳出循环是通过检查寄存器eax来完成的,而没有检查变量busy所在内存的真实内容。注意,对于这段代码来说这里的优化是正确的,但问题是如果还有其它代码修改了变量busy,那么这里的优化会导致其它代码对变量busy的修改根本就不能生效,就像这样:intbusy=l;该函数在A线程中执行voidwait()while(busy);)该函数在B线程中执行VoidsignalObusy=O;)如果Wait函数中While循环对应的机器指令仅仅从寄存器中读取数据那么即使B线程的signal函数修改了busy变量也不能让wait函数从循环中跳出来。如果你对busy变量使用volatile修饰,生成的指令就变成这样了:A8Sm<Md+Addnew.Vrtm©Cx86-64gcc12.1-©-02-4 volatileintbw>yX;4.Output.*TfiltwBioranei÷Addnew.ZAddtooL-1voidwit()Il*it:wait:.L2:moveax,DWORDPTRbusyrip|testeax,eaxjne.L2retbusy:.longl注意看此时L2这一段,每次都从busy变量所在的内存中读取数据并存放在eax,然后再去判断,这样就能确保每次都能读取到busy变量的最新值。实际上你可以把寄存器eax当做busy所在内存的cache,当CaChe(寄存器)和内存中的数据一致时不会有任何问题,但当cache与内存中的数据不一致时(也就是内存已被更新但cache保存的还是旧数据),程序的运行往往出乎预料除了多线程的例子,还有一类就是signalhandler以及硬件修改该变量(用C语言与硬件交互式时经常遇到),如果编译器生成文章开头那样的指令那么等待线程将检测不到Signalhandler或者硬件对变量的修改。 DOoooosy buMemory国Hardware因此在这里我们需要告诉编译器:“不要耍小聪明,不要只从寄存器中读数据,这个变量可能在其它地方已经被修改了,使用时从内存中获取最新数据”。现在是时候简单总结一下了,volatile仅仅阻止编译器试图去优化对变量的读取操作。volatile与多线程一定要注意VoIatile仅仅确保变量的可见性,但和变量的原子访问没有半毛钱关系,这是两个完全不同的任务。假设有一个非常复杂的结构体StrUCtfoo:Structdatainta;intb;intc;.;Volatilestructdatafbo;voidthread1()foo.a=1;fbo.b=2;fbo.c=3;.voidthread2()inta=fbo.a;intb=foo.b;intc=fbo.c;.你仅仅用volatile去修饰变量foo只是确保了当该变量被thread1修改后我们能在thread2中读取到最新值,但是这解决不了多线程并发读写需要原子访问foo的问题。确保变量原子性访问一般都采用锁,当使用锁时,锁本身就包含了volatile提供能力,即,确保变量的可见性,因此当使用锁时没有必要使用volatile。volatile与memoryorder有的同学可能会想如果我想用VoIatiIe修饰的变量没有那么复杂,仅仅是一个int,就像这样:Volatileintbusy=O;A线程读取busy变量,B线程更新busy变量,当A检测到busy变化后执行特定操作,这样可行吗?既然通过VOlatiIe修饰后可以确保每次都从内存中读取busy,那么应该可以这样使用吧。然而,计算机在概念上可能相对简单些,但在工程实践中是复杂的。我们知道由于CPU与内存之间的速度差异非常大,CPU与内存之间有一层cache,CPU其实并没有直接读取内存,CaChe的存在会让问题复杂起来,限于篇幅与本文主题这里不再展开。为优化内存读写,CPU可能会对内存读写操作进行指令重排,reordering,带来的后果就是:假设在线程1中先后执行第N行代码与第N+1行代码,但在线程2看来却是第N+1行代码先生效,假设X的初始值为O,Y的初始值为1:线程1线程2X=10if(!busy)busy=0;Y=X;当线程2检测到busy为0后读取X的值,此时读取到的X值可能为0。为解决这一问题,我们需要的不是volatile,volatile解决不了reordering问题,我们需要的是内存屏障,Inemorybarriero内存屏障是一类机器指令,该指令对处理器在该屏障指令之前与之后的内存操作进行了限制,确保不会出现重排问题。而内存屏障带来的效果依然能够涵盖volatile提供的功能,因此也不需要volatileo可以看到,在多线程环境下我们几乎总是不会使用VoIatiIe关键字。许多程序员无法正确的理解C语言关键字VoIati匕。这并不奇怪,大多数C原因书籍不过一两句一带而过。本文将告诉你如何正确使用它。在C/C+嵌入式代码中,你是否经历过下面的情况:代码执行正常一直到你打开了编译器优化代码执行正常一直到打开了中断古怪的硬件驱动RTOS的任务独立运行正常一直到生成了其他任务如果你的回答是“yes”,很有可能你没有使用C语言关键字VOlatileo你并不是唯一的,很多程序员都不能正确使用VOlatileo不幸的是,大多数C语言书籍对volatile的藐视,只是简单地一带而过。volatile是用于声明变量时的使用限定符。它告诉编译器该变量值可能随时发生变化,且这种变化并不是代码引起的。给编译器这个暗示是很重要的。在开始前,我们向来看一看VOIatile的语法。C语言关键字volatile语法声明一个变量为volatile,可以在数据类型之前或之后加上关键字volatileo下面的语句,把foo声明一个VOIatiIe的整型。Volatileintfoountvolatilefoo;把指针指向的变量声明为volatile很常见,尤其是1/0寄存器的地址映射。下面的语句,把pReg声明为一个指向8-bit无符号指针,指针指向的内容为Volatileovolatileuint8_t*pReg;uint8_tvolatile*pReg;volatile的指针指向非VoIatiIe的变量很少见(我只使用过一次),但我还是给出相应的语法。int*volatilep;顺便提一下,关于为什么要在数据类型前使用VOlatiIe关键字,请自行百度搜素。最后,如果你在StnJCt或者union前使用VOlatiIe关键字,表明StrUCt或者Union的所有内容都是VOIatile。如果这不是你的本意,可以在StrUCt或者Union成员上使用volatile关键字。正确使用C语言关键字volatile只要变量可能被意外的修改,就需要把该变量声明为Volatile。实际应用中,只有三种类型数据可能被修改。1 .外设寄存器地址映射2 .在中断服务程序中修改全局变量3 .在多线程、多任务应用中,全局变量被多个任务读写我们将分别讨论上述三种情况。外设寄存器嵌入式系统包含真正的硬件,通常会有复杂的外设。这些外设寄存器的值可能被异步的修改。举个简单的例子,我们要把一个8-bit状态寄存器的地址映射到0x1234.在程序中循环查看该状态寄存器的值是否变为非0.下面是最容易想到,但错误的实现方法1uint8t*PReg=(uint8t*)0x1234;23 /vatforregistertobecomenon-zero4 while(*pReg=0)/dosomethingelse当你打开编译器优化时,程序总是执行失败。因为编译器会生成下面的汇编代码:1movptr,#0x12342 mova,ptr34 loop:5 bzloop程序被优化的原因很简单,既然已经把变量的值读入累加器,就没有必要重新写一遍,编译器认为值是不会变化的。就这样,在第三行,程序进入了无限死循环。为了告诉编译器我们的真正意图,我们需要修改函数的声明:1uinit8_tvolatile*pReg=(uint8_tvolatile*)0x1234;编译器生成的汇编代码:1movptrj#0x123423 loop:4 mova,ptr5 bzloop像这样,我们得到了正确的动作。中断服务程序在中断服务程序中,经常会修改些全局变量值,来作为主程序中的判断条件。例如,在串口中断服务程序中,可能会检测是否接收到了ETX(假如是消息的结束标识符)字符。如果接收到了ETX,ISR设置一个全局标志位。错误的做法:1 inttjrcvdFMSS.2 void>aln()S14 .5 VMmueV4)6 (?/wait12 interrupteid(void)13 14 .lif(FIX=njchn)16(1?txscd三TWJE.18)W.20I在关闭编译器优化的情况下,程序可能执行正常。然而,任何像样点而优化都会"break”这段程序。问题是编译器并不知道etx.rcvd可能被ISR中被修改。编译器只知道,表达式!ext,CVd始终为真,你将永远无法退出循环。结果,循环后面的代码可能被编译器优化掉。幸运的话,你的编译器可能会发出警告;不幸的话,(或者你不认真的查看编译器警告),你的程序无法正常执行。当然,你可以责怪编译器执行了“糟糕的优化二解决方式是,将变量etx_rCVd声明为VolatiIe,所有问题(当然,也可能是部分)就消失了。多线程应用在实时系统中,尽管有像queues,pipes等这些同步机制,使用全局变量实现两个任务共享信息的做法依然很常见。即使在你的程序中加入了抢占式调度器,你的编译器依然无法知道什么是上下文切换,或何时发生上下文切换。因此,从概念上讲,多任务修改全局变量的的做法与中断服务程序中修改全局变量的做法是相同的。因此,所有这类全局变量都应该声明为VoIatiIe例如,下面的程序1 intetx-rcvd=FALSE;2 voidnain()3 4 .5 vhile(Jext-rcvd)6 (7 /wait8 9 .10 )1112 interruptvoidrx_isx(void)13 14 .15 if(ETX=rx-chax)16 117 etx_rcvd=TRUE;18 19 .20 当打开编译器优化时,这段程序可能执行失败。解决方法是将cntr声明为Volatileo一些编译器允许你把所有的变量隐式的声明为VOlatileo请抵制这种诱惑,因为它会令你不再思考,当然,也会导致生成低效的代码。另外,也不要责怪优化器或直接把它关掉。现代的优化器已经足够优秀,我已经记不清上次遇到优化bug是什么时候了。相反,我常常看到程序员们错误地使用Volatile0如果你被要求去修改一个很古怪的代码,请在程序中查找一下volatile关键字,如果你什么也没有找到,上面讨论的例子可以向你提供一些解决问题的思路。在C语言编程中,VOEiIe是一个重要的关键字,用于告知编译器变量可能会在意料之外被改变,从而避免编译器对该变量的优化。尽管最常见的用途是在多线程编程中,VoIatile还有一些高级应用。本文将深入探讨VoEiIe关键字的高级应用,提供具体的C语言代码示例并进行讲解。1 .多线程编程中的volatile关键字最常见的用途之一是在多线程编程中,通过volatile关键字告知编译器不要对变量进行优化,以避免出现意外的行为。例如,在多线程环境中,一个线程可能会修改某个变量,而另一个线程在不知情的情况下使用了这个变量。下面是一个示例,展示了在多线程编程中使用volatile的情况:#include<stdio.h>#include<pthread.h>VolatileintsharedValue=O;void*threadFunction(void*arg)SharedValue=10;retumNULL;intmain()pthread_tthread;pthread_create(&thread,NULL,threadFunction,NULL);WhiIe(SharedValUe!=10)等待线程修改SharedValueprintf(,sharedValuehasbeenmodifiedAn");pthread-join(thread,NULL);retum;)在这个例子中,SharedValue变量被声明为volatile,这样可以确保在不同线程中的修改能够被及时反映。2 .嵌入式编程中的volatile关键字在嵌入式编程中,硬件寄存器和内存映射设备常常需要使用volatile关键字,以确保编译器不会对其进行优化,从而保证与硬件的交互是准确的。以下是一个示例,展示了在嵌入式环境中使用VoIatiIe关键字的情况:#include<stdio.h>#defineGP10_PORT(volatileunsignedint*)Ox12345678)intmain()*GPIO_PORT=OxFF;将端口设置为全高电平假设在这里进行了一些与硬件相关的操作UnSignedintVaIUe=*GP1O_PORT;读取端口的值printf("ValuereadfromGPIO_PORT:%un",value);retum;)在这个例子中,GPleLPoRT是一个硬件寄存器的内存地址,通过将其声明为VOlatiIe,确保编译器不会对读写操作进行优化。3 .优化编译器优化在某些情况下,我们可能希望关闭编译器的某些优化,以便更好地进行调试或者对代码进行性能分析。volatile关键字可以在这方面发挥作用。以下是一个示例,展示了如何使用VOIatile来关闭编译器优化:#include<stdio.h>volatileintdebugFlag=0;voiddebugPrint(constchar*message)if(debugFlag)printf(,Debug%sn,message);intmain()debugFlag=1;debugPrint(Thisisadebugmessage.);retum;)在这个例子中,debugFlag变量的值是不确定的,因此编译器不会对debugPrint函数进行优化,即使debugFlag在函数调用前被修改。4 .指针类型转换有时候,我们可能需要在指针类型之间进行转换,而编译器会认为这是不安全的操作,从而导致编译错误。使用VoEile关键字可以告知编译器,这个类型转换是有意义的,不应该引发错误。以下是一个示例:include<stdio.h>intmain()intvalue=42;int*volatilevolatileIntPtr=&value;void*voidPtr=(void*)volatileIntPtr;int*newValuePtr=(int*)voidPtr;printf(,Newvalue%dn,*newValuePtr);retum;)在这个例子中,将int指针转换为Void指针,然后再转回血指针。在这种情况下,使用VolatiIe关键字可能会更合适,因为编译器不会对Void指针的转换进行优化。volatile关键字在C语言中有许多高级应用,从多线程编程到嵌入式环境中的硬件交互,再到调试和指针类型转换。通过使用VOlatile关键字,我们可以告知编译器在某些情况下不要进行优化,从而确保代码的正确性和准确性。本文的示例代码和讲解希望能够帮助读者更好地理解volatile关键字的高级用法,并在实际项目中应用这些概念。C+volatile在多线程中的作用在C+篇程中,VOlatile关键字是一种类型修饰符,用于告诉编译器对象的值可能会在编译器无法检测到的情况下被改变。这通常在处理与硬件交互或共享内存的多线程程序时出现。本文将深入探讨VoIatile的作用,解释为什么它是重要的,并展示如何在代码中使用它。二、VOIatiIe关键字的重要性在多线程编程和硬件交互中,数据可能在一个线程或中断服务程序中改变,而另一个线程或主程序并不知道这种改变。这可能导致数据不同步,甚至产生不可预知的行为。volatile关键字可以确保编译器不会对这些变量进行优化,从而避免这类问题。三、volatile的使用场景多线程环境:在多线程环境中,一个线程可能正在修改一个变量,而另一个线程可能正在读取它。如果没有使用VOIatiIe关键字,编译器可能会进行优化,导致读取的线程无法立即看到修改后的值。硬件寄存器交互:与硬件寄存器交互时,通常需要使用VOEiIe关键字。这是因为硬件寄存器的值可能会在程序执行期间发生变化,而这些变化可能无法被编译器检测到。中断服务程序:在中断服务程序中修改的变量需要使用VOlatiIe关键字,以确保主程序能够正确地读取这些变量的值。四、如何在代码中使用volatile下面是一个简单的示例,展示了如何在代码中使用VOlatiIe关键字:#include<iostream>#include<thread>#include<chrono>#include<atomic>声明一个volatile变量volatileboolflag=false;VoidsetFIagOstd:this_thread:sleep_fbr(std:chrono:seconds(1);flag=true;修改volatile变量的值intmain()std:threadt(setFlag);在另一个线程中设置flag的值While(!flag)/主线程循环等待flag变为truestd:cout«"Waitingforflagtobeset.n«std:endl;)std:xout<<Flaghasbeenset!<<std:endl;t.join();retumO;在这个示例中,我们创建了一个全局的volatile布尔变量flag。在另一个线程中,我们等待一秒钟,然后将flag设置为trueo在主线程中,我们循环等待flag变为trueo由于flag被声明为volatile,编译器不会对其进行优化,从而确保主线程能够看到另一个线程对flag的修改。五、注意事项虽然volatile关键字可以确保变量的可见性,但它并不能解决所有的并发问题。例如,它不能确保复合操作的原子性。在这种情况下,应该使用互斥锁或其他同步机制来确保数据的正确性。此外,过度使用VOIatiIe可能会导致性能下降,因为它会阻止编译器进行优化。因此,在使用VOlatiIe时应该谨慎权衡其利弊。六、结论本文深入探讨了C+中VoIatiIe关键字的作用和重要性。我们讨论了为什么在多线程编程和硬件交互中需要使用volatile,并展示了如何在代码中使用它。我们还强调了在使用volatile时需要注意的问题和权衡其利弊的重要性。通过理解并正确使用VOIatile关键字,我们可以编写更加健壮和可靠的并发和多线程程序。