也来说说C/C++里的volatile关键字
去年年底的样子,何登成写了一篇关于C/C++ volatile关键字的深度剖析blog(C/C++ Volatile关键词深度剖析)。全文深入分析了volatile关键字的三个特性。这里不想就已有内容再做一遍重复,而是再提供一些自己的看法,以完善对volatile的全面认识。
前文一个很好的例子就是:
在这个例子里事实上还引入的另外一个问题,就是多线程环境里该如何使用volatile?
要全面回答这个问题,没那么容易。不过一个已经被很多人接受的结论已经有了,并且很具有权威性。这个结论来自于Linux kernel documention。
C programmers have often taken volatile to mean that the variable could be
changed outside of the current thread of execution; as a result, they are
sometimes tempted to use it in kernel code when shared data structures are
being used. In other words, they have been known to treat volatile types
as a sort of easy atomic variable, which they are not. The use of volatile in
kernel code is almost never correct.
说明白点就是,在linux kernel这种大型并且复杂的系统编程项目里,不能使用volatile,除非能给出强有力的证据!所以我们的项目中,几乎可以肯定,根本没有使用的必要。
结论已经有了,接下来就是阐述为什么了。
在多线程中使用volatile,很多情况下就是为了解决共享数据的访问问题。比方说上面这个例子,如果不使用volatile,那么编译器生成的代码在访问flag变量时,很可能都是从缓存(寄存器)中读取的。某个线程对flag的修改,无法通知到另外一个线程。为此需要使用volatile,保证每次读写都需要有内存访问。这体现了volatile的易变性和不可优化性。与此同时,也引出了一个疑问:volatile的这些特性确信是解决该问题的正确方案么,或者说就没有其他可选的解决方案了么?
显然volatile不能解决这个问题,因为还存在编译器优化和CPU执行指令时的乱序情况(Out-of-Order Execution, OOE。不过上面这个例子在x86-based机器上不会发生OOE的情况,可以看这里了解x86-based CPU乱序的总结)。
所以说,多线程下访问共享数据至少要考虑两点(应该还有其他要考虑的,但是写到这里,我只能列这些):
- 数据一致性。保证每次读到的都是最新的数据,每次写都是基于最新的数据。
- 指令执行在某种程度上的顺序性。
而volatile关键字根本不能保证这两点内容。所以volatile在多线程下根本没有用。因为volatile类型的数据并不保证数据读写的原子性。并且volatile关键字生成的代码一般情况下不会附带上特殊的CPU指令。因此volatile至少不能控制CPU的乱序执行。
我们再从一个简单的场景来考虑这个问题。假设有多个线程会去读写同一个变量a。我们通常的做法是怎么样的?对,使用锁。为什么?这么高深的问题,我只能借用别人的研究成果了:
Like volatile, the kernel primitives which make concurrent access to data
safe (spinlocks, mutexes, memory barriers, etc.) are designed to prevent
unwanted optimization.
结论来了,如果有锁在,和锁相关的代码是会被特殊考虑的,不该有的优化是会被屏蔽的(应该是编译器的代码生成和CPU执行指令两方面都有影响)。所以你希望volatile能做的事情(虽然它做不到),锁或者内存屏障都能做。并且在使用这些工具的时候,根本不需要volatile的参与。
至此,关于不需要使用volatile的论证基本就结束了。
回到何的文章,后面还介绍了为什么会有volatile这个关键字,这个关键字解决了什么问题。这里想补充说的是,volatile关键字并不是定义了一个和数据内容相关的属性;volatile关键字是定义了一个和数据访问相关的属性。从当初volatile被设计为用于MMIO(Memory Mapped IO)以及C/C++最初并不包含多线程的概念可以看出volatile并不是为了多线程而设计的。因此将volatile应用于多线程本身就不合适。
In C, it's "data" that is volatile, but that is insane. Data
isn't volatile - _accesses_ are volatile. So it may make sense to say
"make this particular _access_ be careful", but not "make all accesses to
this data use some random strategy".
UPDATE: 2014-1-22
这里再补充点Visual C++关于volatile关键字的特别之处。
Visual C++ 2005之后,volatile关键字和其他高级语言,比方说C#会比较接近。直接来看MSDN的描述:
Objects declared as volatile are not used in certain optimizations because their values can change at any time. The system always reads the current value of a volatile object at the point it is requested, even if a previous instruction asked for a value from the same object. Also, the value of the object is written immediately on assignment.
Also, when optimizing, the compiler must maintain ordering among references to volatile objects as well as references to other global objects. In particular,
- A write to a volatile object (volatile write) has Release semantics; a reference to a global or static object that occurs before a write to a volatile object in the instruction sequence will occur before that volatile write in the compiled binary.
- A read of a volatile object (volatile read) has Acquire semantics; a reference to a global or static object that occurs after a read of volatile memory in the instruction sequence will occur after that volatile read in the compiled binary.
This allows volatile objects to be used for memory locks and releases in multithreaded applications.
所以Visual C++ 2005后,volatile对象可以用作memory barrier。
当然,C++11标准后,情况又有了新的变化。在VC没有支持C++11标准前(VC2010及以前) ,对volatile关键字的描述中明确指明了volatile关键字是可以用于解决多线程数据访问的问题的:
The volatile keyword is a type qualifier used to declare that an object can be modified in the program by something such as the operating system, the hardware, or a concurrently executing thread.
但是C++11标准明确了volatile的定义,让他回归了当初设计的本源:
A type qualifier that you can use to declare that an object can be modified in the program by the hardware.
the C++11 ISO Standard volatile keyword is different and is supported in Visual Studio when the /volatile:iso compiler option is specified. (For ARM, it's specified by default). The volatile keyword in C++11 ISO Standard code is to be used only for hardware access; do not use it for inter-thread communication. For inter-thread communication, use mechanisms such as std::atomic<T> from the C++ Standard Template Library.
UPDATE: 2014-2-11
关于memory reordering,Jeff Preshing的这篇文章值得深入阅读。特别是comments部分里罗列的一些资源。这些额外的链接讨论了一个非常有趣的问题,并且不同的人有不同的看法。有人认为该用volatile解决reordering问题。
这个问题是这样的。有如下一段代码:
extern int v;
void f(int set_v)
{
if (set_v) v = 1;
}
GCC 3.3.4 - 4.3.0带有O1优化的情况下,汇编码是:
f:
pushl %ebp
movl %esp, %ebp
cmpl $0, 8(%ebp)
movl $1, %eax
cmove v, %eax ; load (maybe)
movl %eax, v ; store (always)
popl %ebp
ret
从汇编看,即使调用f(0),也会存在一次写v的动作。在多线程环境下,即便f(0)也要加锁(v在多线程下是共享数据)。
这个问题的讨论中,有人认为这是编译器bug;有人认为v应该加上volatile修饰,这样就不会生成这样的汇编码了。
结论是,这个是编译器的bug。
完!
Reference:
- Why the “volatile” type class should not be used
- doc: volatile considered evil
- Lockless Programming Considerations for Xbox 360 and Microsoft Windows
- volatile (C++) 2013
- volatile (C++) 2005
- Optimization of conditional access to globals: thread-unsafe?
- Single Threaded Memory Model
- -fno-tree-cselim not working?
posted on 2014-01-11 21:10 wpcockroach 阅读(2923) 评论(0) 编辑 收藏 举报