• 理解volatile关键字(zz)


    一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
    1) 并行设备的硬件寄存器(如:状态寄存器)
    2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
    3) 多线程应用中被几个任务共享的变量

    编译器在编译代码时,会进行自动优化,用以产生优化指令。同上操作系统和一些线程同样也会对你所定义的一些变量做出一些你所不知道的更改。这样的更改我们称为,隐式修改,因为你不知道,编译器在什么情况下,在那里做出了优化,甚至你都不知道,或是不能肯定编译器到底有没有对你的代码做出优化。

    int main(int argc, char* argv[])
    {
        int i=10;
        int a = i;

        printf("i= %d \n",a);

        __asm {
            mov dword ptr [ebp-8], 50h
        }
        int b = i;
        printf("i= %d \n",b);
        return 0;
    }

    在调试版本(debug)模式运行程序,输出结果如下:
    i = 10
    i = 80

    在release版本模式运行程序,输出结果如下:
    i = 10
    i = 10

    但是如果我们把i的声明前面加上volatile,那么输出的结果是什么呢?自己试试吧,我在VS2010中试的结果是volatile并没什么作用。

    回答下面几个问题:

    1)一个参数既可以是const还可以是volatile吗?解释为什么。
    2) 一个指针可以是volatile 吗?解释为什么。
    3) 下面的函数有什么错误:
    int square(volatile int *ptr)
    {
    return *ptr * *ptr;
    }
    下面是答案:
    1) 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。这里牵扯到对const关键字的理解,请移步http://www.cnblogs.com/whyandinside/archive/2012/09/11/2680322.html

    2) 是可以的。比如说,我们有一个指针,用来指向两个buffer中的一个,并且轮换使用这两个buffer,这样多线程使用时,就要避免我们对这个指针的解引用被cache住了,而使用了错误的值;
    3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
    int square(volatile int *ptr)
    {
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
    }
    由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
    long square(volatile int *ptr)
    {
    int a;
    a = *ptr;
    return a * a;
    }

    volatile并不能保证在多线程程序中的正确性, 下面先理解原子操作:

    原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的"读-改-写"过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:

    1. CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值(2)装载进它的寄存器中;

    2. CPU B从内存单元把当前计数值(2)装载进它的寄存器中;

    3. CPU A在它的寄存器中将计数值递减为1;

    4. CPU B在它的寄存器中将计数值递减为1;

    5. CPU A把修改后的计数值(1)写回内存单元;

    6. CPU B把修改后的计数值(1)写回内存单元;

    我们看到,内存里的计数值应该是0,然而它却是1。如果该计数值是一个共享资源的引用计数,每个进程都在递减后把该值与0进行比较,从而确定是否需要释放该共享资源。这时,两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它--两个进程都推断出:计数值是1,共享资源仍然在被使用。

    http://blog.csdn.net/pengzhixi/article/details/4204397

    中有介绍,我们应该把一个变量使用volatile描述,并且使用原子操作对其进行操作,以保证其正确性。

    http://wenku.baidu.com/view/cd806d2bcfc789eb172dc887.html

    为什么volatile不能代替memory barrier?

    volatile只是避免了1:编译器对其的优化,2:运行时CPU对其的优化;但无法保证其对volatile变量的操作是原子的,那也就意味着,++i的过程中,ThreadA取出i的值,并+1,但是还没来得及写到内存中,被另一个线程抢占了,这时另一个线程也从RAM中读了i,加了1,然后写回RAM;再回到ThreadA,把之前加过的值写回RAM,这时i值少加了一次1,问题就出现了。而如果要保证这一点,就需要使用memory barrier。这篇文章里有个分析,以供参考:http://social.microsoft.com/Forums/pt-BR/visualcpluszhchs/thread/49b09cd2-7516-462a-980c-f4f29680f369

    那是不是说volatile+原子操作就可以解决问题了呢?答案是否定的。volatile并不能保证内存访问的顺序,它只是保证了取这个变量的值的时候都是从内存中读出来的。 而这一点是不够的,因为编译器会根据代码之间的关系对代码进行调整,比如Dekker算法中,想用 flag1/2和turn来实现两个线程情况下的临界区互斥访问。这个算法关键就在于对flag1/2和turn的读操作(load)是在其写操作 (store)之后的,因此这个多线程算法能保证dekker1和dekker2中对gSharedCounter++的操作是互斥的,即等于是把 gSharedCounter++放到临界区里去了。但是,多核X86可能会对这个store->load操作做乱序优化,例如dekker1中对 flag2的读操作可能会被提到对flag1和turn的写操作之前,这样就会最终导致临界区的互斥访问失效,而gSharedCounter++也会因 此产生data race从而出现错误的计算结果。

  • 相关阅读:
    CodeForces 587A
    矩阵快速幂模板
    LCA模板
    Codeforces Round #226 (Div. 2 )
    Codeforces Round #225 (Div. 2)
    SGU132
    SRM 599 DIV 2
    POJ1038
    SGU223
    POJ1185
  • 原文地址:https://www.cnblogs.com/whyandinside/p/2679812.html
Copyright © 2020-2023  润新知