1. 令人困惑的volatile
volatile字面意思是“不稳定的、易失的”,不少编程语言中存在volatile关键字,也有共同之处,如“表示程序执行期间数据可能会被外部操作修改”,如被外设修改或者被其他线程修改等。这只是字面上给我们的一般性认识,然而具体到不同的编程语言中volatile的语义可能相差甚远。
很多人以为自己精通CC++,但是被问起volatile的时候却无法清晰、果断地表明态度,那只能说明还是处在“从入门到精通”的路上,如果了解一门语言常见特性的使用、能够写健壮高效的程序就算精通的话,那实在是太藐视“大师”的存在了。从一个volatile关键字折射出了对CC++标准、编译器、操作系统、处理器、MMU各个方面的掌握程度。
几十年的发展,很多开发者因为自己的偏见、误解,或者对某些语言特性(如Java中的volatile语义)的根深蒂固的认识,赋予了CC++ volatile本不属于它的能力,自己却浑然不知自己犯了多大的一个错误。
我曾经以为CC++中volatile可以保证保证线程可见性,因为Java中是这样的,直到后来阅读Linux内核看到Linus Torvards的一篇文档,他强调了volatile可能带来的坏处“任何使用volatile的地方,都可能潜藏了一个bug”,我为他的“危言耸听”感到吃惊,所以我当时搜索了不少资料来求证CC++ volatile的能力,事后我认为CC++ volatile不能保证线程可见性。但是后来部门内一次分享,分享中提到了volatile来保证线程可见性,我当时心存疑虑,事后验证时犯了一个错误导致我错误地认为volatile可以保证线程可见性。直到我最近翻阅以前的笔记,翻到了几年前对volatile的疑虑……我决定深入研究下这个问题,以便能顺利入眠。
2. 从规范认识volatile
以常见的编程语言C、C++、Java为例,它们都有一个关键字volatile,但是对volatile的定义却并非完全相同。
-
Java中对volatile的定义:
8.3.1.4.
volatile
FieldsThe Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism,
volatile
fields, that is more convenient than locking for some purposes.A field may be declared
volatile
, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).Java清晰地表达了这样一个观点,Java内存模型中会保证volatile变量的线程可见性,接触过Java并发编程的开发者应该都清楚,这是一个不争的事实。
-
CC++中对volatile的定义:
6.7.3 Type qualifiers
volatile: No cacheing through this lvalue: each operation in the abstract semantics must be performed (that is, no cacheing assumptions may be made, since the location is not guaranteed to contain any previous value). In the absence of this qualifier, the contents of the designated location may be assumed to be unchanged except for possible aliasing.
C99中也清晰地表名了volatile的语义,不要做cache之类的优化。这里的cache指的是software cacheing,即编译器生成指令将内存数据缓存到cpu寄存器,后续访问内存变量使用寄存器中的值;需要与之作出区分的是hardware cacheing,即cpu访问内存时将内存数据缓存到cpu cache,硬件操作完全对上层应用程序透明。大家请将这两个点铭记在心,要想搞清楚CC++ volatile必须要先理解这里cache的区别。
C99清晰吗?上述解释看上去很清晰,但是要想彻底理解volatile的语义,绝非上述一句话就可以讲得清的,C99中定义了abstract machine以及sequence points,与volatile相关的描述有多处,篇幅原因这里就不一一列举了,其中与volatile相关的abstract machine行为描述共同确定了volatile的语义。
3. 对volatile持何观点
为了引起大家对CC++ volatile的重视并及时表明观点,先贴一个页面“Is-Volatile-Useful-with-Threads”,网站中简明扼要的告知大家,“Friends don’t let friends use volatile for inter-thread communication in C and C++”。But why?
isocpp专门挂了这么个页面来强调volatile在不同编程语言中的差异,可见它是一个多么难缠的问题。即便是有这么个页面,要彻底搞清楚volatile,也不是说读完上面列出的几个技术博客就能解决,那也太轻描淡写了,所以我搜索、整理、讨论,希望能将学到的内容总结下来供其他开发者参考,我也不想再因为这个问题而困扰。
结合CC++ volatile qualifier以及abstract machine中对volatile相关sequence points的描述,可以确定volatile的语义:
- 不可优化性:不要做任何软件cache之类的优化,即多次访问内存对象时,编译器不能优化为cache内存对象到寄存器、后续访问内存对象转为访问寄存器 [6.7.3 Type qualifiers - volatile];
- 顺序性:对volatile变量的多次读写操作,编译器不能以预测数据不变为借口优化掉读写操作,并且要保证前面的读写操作先于后面的读写操作完成 [5.1.2.3 Program execution];
- 易变性:从不可优化性、顺序性语义要求,不难体会出其隐含着数据“易变性”,这也是volatile字面上的意思,也是不少开发者学习volatile时最熟知