zoukankan      html  css  js  c++  java
  • 编写安全代码:小心volatile的原子性误解

    本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
    ======================================================================================================
    关于volatile的说明,这是一个老生常谈的问题。volatile的定义很简单,将其理解为易变的,防止编译器对其进行优化。那么其用途一般有以下三种:
    1. 外部设备寄存器映射后的内存——因为外部设备寄存器可能会由于外部设备的状态变化而改变,因此映射后的内存需要注明为volatile的;
    2. 多线程或异步访问的全局变量;
    3. 嵌入式汇编——防止编译器对其优化;
     
    这三种用途中的第一种和第三种一般都没有什么疑问。但是关于第二个用途,经常有朋友对其有误解。
     
     
    首先说一下,什么时候情况下全局变量需要有volatile修饰呢?比如,我们大部分的全局变量都是用锁保护的,那么是否还需要volatile呢?答案是否定的,即当全局变量由锁保护的时候,该全局变量是不需要volatile的。原因有二:一是锁保证了临界区的串行;二是锁的实现中有内存屏障,保证临界区访问全局变量为最新值。那么,就可以总结出,当没有锁保护的全局变量是需要使用volatile修饰的,如以下这种情况:
    1. 全局变量int exit_flag = 0;
    2. 线程1的主循环的退出条件是检查exit_flag是否为1,为1,则退出主循环;
    3. 线程2在某些情况下,修改exit_flag为1。
    另外如果是异步情况,即没有线程2,而有一个信号处理函数,在收到指定信号的情况下将exit_flag赋值为1。
    这时,exit_flag都需要使用volatile来修饰。不然对于线程1的代码,如果编译器发现在线程1的代码中没有任何地方修改exit_flag,有可能会将exit_flag放入寄存器缓存。这样,在每次的条件检查的时候,都是从寄存器中读取,而非exit_flag对应的内存。这样将导致每次读取的值都为0,从而导致thread1无法退出。而使用volatile修饰exit_flag,会避免编译器做这种优化,强制每次读取都是从内存中读取,这样就可以保证了在exit_flag置为1时,thread1可以读取到最新值而退出。
     
    那么现在有这样的一个问题。当有一个全局变量volatile int counter = 0作为一个计数器,而且同时有两个线程会修改这个计数器,这时这个counter是否需要锁的保护呢?
    比如下面的代码:
    1. static volatile int counter = 0;
    2. void add_counter(void)
    3. {
    4.     ++counter;
    5. }
    在多线程的环境下,是否安全呢?counter的递增是否为我们所期待的结果呢?请大家先想一分钟!
     
    为了得出答案,让我们看汇编代码:

    1. add_counter:
    2. pushl %ebp
    3. movl %esp, %ebp
    4. movl counter, %eax
    5. addl $1, %eax
    6. movl %eax, counter
    7. popl %ebp
    8. ret
    看红色的这三行。怎么回事?为什么还是先把counter存入了寄存器eax,然后对eax进行操作,再将eax存入counter。这样的话,++counter就绝不会是原子性操作!必须由锁保护!
     
    下面看看为什么汇编是这样的行为。究竟volatile提供了什么样的服务?!
    首先修改一下之前的C代码:

    1. static int counter = 0;
    2. void add_counter(void)
    3. {
    4.     for (; counter != 0x10; ++counter) {
    5.         ++counter;
    6.     }
    7. }
    然后看其汇编代码,记得要是使用-O优化啊。
    [fgao@fgao-vm-fc13 test]$ gcc -S -O test.c

    1. add_counter:
    2. pushl %ebp
    3. movl %esp, %ebp
    4. movl counter, %eax
    5. cmpl $16, %eax
    6. je .L4
    7. .L5:
    8. addl $2, %eax
    9. cmpl $16, %eax
    10. jne .L5
    11. movl %eax, counter
    12. .L4:
    13. popl %ebp
    14. ret
    从上面的汇编代码中,可以很清楚的看出:这时将counter的值存入eax,然后每次给eax加2,然后使用eax与16比较,来完成C代码中的for循环的工作。
     
    下面给counter加上volatile修饰符

    1. static volatile int counter = 0;
    2. void add_counter(void)
    3. {
    4.     for (; counter != 0x10; ++counter) {
    5.         ++counter;
    6.     }
    7. }
    依然打开编译器的优化开关
    [fgao@fgao-vm-fc13 test]$ gcc -S -O test.c

    1. add_counter:
    2. pushl %ebp
    3. movl %esp, %ebp
    4. movl counter, %eax
    5. cmpl $16, %eax
    6. je .L4
    7. .L5:
    8. movl counter, %eax
    9. addl $1, %eax
    10. movl %eax, counter
    11. movl counter, %eax
    12. addl $1, %eax
    13. movl %eax, counter
    14. movl counter, %eax
    15. cmpl $16, %eax
    16. jne .L5
    17. .L4:
    18. popl %ebp
    19. ret
    L5为对应C代码中的for循环的汇编实现。通过对比前一个汇编,我们很容易就发现了区别。虽然在执行counter递增的时候,两者都使用了寄存器eax。但是在每次访问counter的时候,前者会直接使用寄存器eax,而后者(使用volatile的时候)会重新从counter中读取最新值至eax,然后再使用eax。
     
    OK。现在volatile的情况已经明了。volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。那么编译器究竟是直接修改内存的值,还是使用寄存器修改都符合volatile的定义。所以,一句话,volatile并不提供原子性的保证。
  • 相关阅读:
    cocos2dx打包apk
    cocos2d 小游戏
    排序算法笔记二
    把一张合成图分拆出各个小图
    出栈入栈动画demo
    Android 面試題
    AS项目删减打包-01
    c程序指针题
    ubuntu14.04 设置默认登录用户为root
    Ubuntu14.04 Java环境变量配置
  • 原文地址:https://www.cnblogs.com/biglucky/p/5499384.html
Copyright © 2011-2022 走看看