发布者: Bo-tao Zhang

C/C++中volatile的传统用途

保证变量的读写操作直接作用于内存

即每次读的时候都直接去该变量的内存地址中读,每次写的时候都直接写到该变量的内存地址中去。 看下面的例子,如果没加volatile的话,下面这段代码中变量flag的读操作可能会被编译器优化掉,但如果加了volatile则不会。

bool flag = false;
void fun()
{
    while(!flag)
    {
        // do something
    }
}
 
// flag未加volatile 被优化后相当于下面这样
bool flag = false;
void func()
{
    if (!flag) // read only once
    {
        while(true)
        {
            // do something
        }
    }
}

阻止编译器打乱变量之间的读写顺序。

注意:只能够保证volatile变量之间的操作顺序,不保证volatile与非volatile变量之间的操作顺序不被编译器改变。 第一个例子,两个变量都没加volatile, 他们的操作顺序被编译器打乱。

第一个例子

第二个例子,只有一个变量是volatile, 另一个不是。 它们的操作顺序还是被改变。

第二个例子

第三个例子,两变量都是volatile, 它们的操作顺序未被改变。

第三个例子

注意: 虽然volatile变量之间的操作顺序不会被编译器打乱,但是还是有可能被CPU打乱, 具体要看CPU的内存模型。这也是volatile不能用于线程同步的原因。

阻止编译器对变量做激进的优化。比如直接去掉某变量相关的指令。

int func()
{
    int x = 1;// 如果未加volatile的话,x, y两变量会直接被优化掉
    int y = 2;
    int a = x + y;
    printf("a = %d", a);
}

volatile在多线程中的误用

因为volatile能够保证变量的读写操作直接作用于内存。所以很多人就想当然的认为可以用volatile的这种特性 来对两线程进行的同步。比如一个经典的错误用法是:用一个volatile flag来同步两个线程。一个线程准备好 数据之后设置flag的值,另一个线程则不停的check flag 的值,根据flag的值来判断数据是否可读。听起来好像 没什么问题,但是大家想一想,万一其中一个线程内的指令被打乱使得在数据还没准备好就设置了flag的值, 这时另一个线程就会读到一些没有初始化的数据,从而导致意想不到的错误。 看下面这个例子:

volatile int flag = 0;
volatile int data;
void Write() // thread one
{
    data = 2;
    flag = 1;
}
void Read() // thread two
{
    while(!flag)
    {
        printf("data = %d\n", data);
    }
}

上面这段代码在X86上没问题,在其他平台上则可能会出现错误。之所以错误是因为 函数write里面的操作data = 2;flag = 1;的执行顺序可以被CPU打乱。注意,我说的是可能被打乱, 至于到底会不会被打乱得看CPU的内存模型。在X86下面是不会被打乱的,因为在X86里面不容 许写写乱序,只容许写读乱序。但是在PowerPC下则会乱序。 下面这张表格记录了各个CPU中可能会被乱序的操作。从表中不难看出在X86下,不容许写写乱序。 注意:X86_64 属于AMD64,也不容许写写乱序。

Memory ordering in some architectures

非标准的volatile

前面讲了C/C++中volatile的作用, 传统的volatile对指令的执行顺序无法保证。但是Visual C++和Java里面 对传统的volatile功能进行了拓展。在Visual C++里面,volatile变量的读操作具有Acquire语义而写操作具有 Release语义。Acquire语义是指:紧跟在volatile变量的读操作之后的所有操作必须等到volatile变量的读操作 执行完之后才开始执行,绝不容许将其提到volatile的读操作之前执行。Release语义是指:volatile变量的 写操作之前的所有操作必须在volatile变量的写操作之前执行,绝不容许提到volatile变量的写操作之后执行。 正因为如此,所以下面这段代码在visual C++ 中也是安全的。在write 线程中,是对两个volatile变量进行写操作, 因为写操作具有Acquire 语义所以不能乱序,而在Read线程中是对两个volatile变量进行读操作,因为读操作具有 Relese语义,所以也不能乱序。因此,下面这段代码在visual C++ 中也是安全的。

volatile int flag = 0;
volatile int data;
void Write()
{
    data = 2;
    flag = 1;
}
void Read()
{
    while(!flag)
    {
        printf("data = %d\n", data);
    }
}

总结

以上讲到了C/C++中volatile的三种基本用法以及volatile在多线程中的误用。 还顺便提到了Visual C++ 和Java中对volatile传统功能的拓展。



-EOF-
睿初科技软件开发技术博客,转载请注明出处

blog comments powered by Disqus