编写安全代码:小心volatile的原子性误解

本文的copyleft归[email protected]所有,使用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优化啊。

[[email protected] 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. }

依然打开编译器的优化开关

[[email protected] 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并不提供原子性的保证。

时间: 2025-01-16 10:39:40

编写安全代码:小心volatile的原子性误解的相关文章

编写可读性代码的艺术

在做IT的公司里,尤其是软件开发部门,一般不会要求工程师衣着正式.在我工作过的一些环境相对宽松的公司里,很多程序员的衣着连得体都算不上(搞笑的T恤.短裤.拖鞋或者干脆不穿鞋).我想,我本人也在这个行列里面.虽然我现在改行做软件开发方面的咨询工作,但还是改不了这副德性.衣着体面的其中一个积极方面是它体现了对周围人的尊重,以及对所从事工作的尊重.比如,那些研究市场的人要表现出对客户的尊重.而大多数程序员基本上每天主要的工作就是和其他程序员打交道.那么这说明程序员之间就不用互相尊重吗?而且也不用尊重自

不要将时间浪费到编写完美代码上(转)

add by zhj:文章主要说的是代码会经常变化,追求完美只会浪费更多时间. (英文:DZone,译者:raygodlee) 不要将时间浪费到编写完美代码上,原因就在于一个系统的迭代开发可能持续运行5年至10年甚至是20年,而某行代码甚至某个设计的生命周期则要短很多,只有几个月或者几天.所以说有没有必要将大量时间花在写代码上? 一个系统的迭代开发可能持续运行5年至10年甚至是20年.相比之下,某行代码甚至某个设计的生命周期则要短很多,只有几个月或者几天,甚至当你为了解决一个问题迭代测试不同方案

[转载]编写优秀代码的10个技巧

作为程序员,写代码是需要一种崇高无上的精神来支撑的,写出优秀的代码,更需要你有深厚的底蕴和良好的编码习惯.在介绍写优秀代码的10个技巧之前,我们先来探讨一下什么样的代码才是优秀的代码. 稳定可靠(Robustness) 可维护且简洁(Maintainable and Simple Code) 高效(Fast) 简短(Small) 共享性(Reusable) 可测试性(Testable) 可移植性(Portable) 面对以上的目标,我们总结了以下10个写代码的技巧,希望对你有所帮助. 1.百家之

最新的JavaScript核心语言标准——ES6,彻底改变你编写JS代码的方式!【转载+整理】

原文地址 本文内容 ECMAScript 发生了什么变化? 新标准 版本号6 兑现承诺 迭代器和for-of循环 生成器 Generators 模板字符串 不定参数和默认参数 解构 Destructuring 箭头函数 Arrow Functions Symbols 集合 学习Babel和Broccoli,马上就用ES6 代理 Proxies ES6 说自己的宗旨是"凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明."--TRUE!如果你大概浏览下 ES6 的新特性,事实上它

实时显示iOS编写UI代码效果

编写iOS应用UI的方式大概有两种,一种是Storyboard/Xib,另一种是手写代码.采用Storyboard/Xib方式组织UI,由于提供可视化的特性,只要从UI库中拖动UI控件,便可以显示结果,极大地提高开发速度.但面临一个问题就是多人协作开发,由于所有的UI都放在同一个Storyboard文件中,使用Git/SVN合并代码就会出现冲突.多人协作开发还不是主要问题,有人提出可以创建多个Storyboard来分开UI编写,而Storyboard/Xib最主要问题是代码复用性比较差.所以有些

第1课第4.4节_Android硬件访问服务编写HAL代码

4 编写HAL代码 源码下载方法 第一次: git clone https://github.com/weidongshan/SYS_0001_LEDDemo.git 更新: git pull origin 取出指定版本: git checkout v1 // 有JNI没有HAL git checkout v2 // 有JNI,HAL git checkout v3 // add MODULE TAG, DEVICE TAG JNI 向上提供本地函数, 向下加载HAL文件并调用HAL的函数HAL

JNI技术基础(2)——从零开始编写JNI代码

书接上文: <JNI技术基础(1)——从零开始编写JNI代码> 2.编译源程序HelloWorld.java并生成HelloWorld.class 3.生成头文件HelloWorld.h 在Linux控制台输入命令:javah –jni HelloWorld 生成HelloWorld.h头文件 //HelloWorld.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header

编写css代码的方式

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <!-- html 在一个网页中负责的事情是一个页面的结构 c

C#和JAVA中编写事务代码

C#  DAL层代码,执行多条增删改,使用事务操作: /// <summary> /// 执行 多条增删改 (非查询语句) /// </summary> /// <param name="strSql"></param> /// <param name="paras"></param> /// <returns></returns> public static int E