在嵌入式编程中,有对某地址重复读取两次的操作,如地址映射IO。但如果编译器直接处理p[0] = *a; p[1] = *a这种操作时,往往会忽略后一个,而直接使用前一个已计算的结果。这是有问题的,因为地址a由于映射了端口,每一次读取都不同,都必须从地址上读取,不能让编译器进行优化。volatile因此而生。加了volatile的变量,编译器生成二进制代码时,每次都从源码意图取的地方去取,不优化。
 
但,后面有人妄想使用volatile每次从真实地址读取的特性而在多线程中使用,初始衷是在其他线程变化的共享变量,也能在当前线程中立即显现。这是误用。多线程共享变量的正确做法是加锁,不应创造发明线程间同步方法。
 
有这想法的人有个基本假设,认为CPU会老实按顺序执行编译出的二进制代码。
以下引入内存访问模型:
 
Memory consistency models,内存访问模型
内存访问模型描述的是硬件架构保证的内存的访问顺序。
对于程序员最习惯内存模型是顺序一致(sequential consistency),即上文所说的volatile使用于多线程者的默认假设:
- 所有的内存操作看起来和一次操作一样;
- 对单个CPU,内存操作的顺序与CPU执行代码的顺序一致。
 
就像:
Thread 1 Thread 2
A = 3
B = 5
reg0 = B
reg1 = A
设默认内存地址上值为0,那么结果的可能性是这样的:
 
Registers States
reg0=5, reg1=3 possible (thread 1 ran first)
reg0=0, reg1=0 possible (thread 2 ran first)
reg0=0, reg1=3 possible (concurrent execution)
reg0=5, reg1=0 never
可见,在顺序一致的内存模型中,不可能出现第4种情况,因为B的写入在A的后面。往往写代码时默认的是这种内存模型,大多单CPU架构上,包含ARM和X86确实是,但是,绝大多数的SMP架构上,不是顺序一致模型。
 
 
X86 SMP内存模型是处理器一致(processor consistency),它比顺序一致稍弱。对于单独的读、写,是顺序一致的,但它不保证先写后读的顺序
ARM SMP更甚,它也不保证单独读写的顺序。
 
考虑以下例子:
Thread 1 Thread 2
A = true
reg1 = B
if (reg1 == false)
    critical-stuff
B = true
reg2 = A
if (reg2 == false)
    critical-stuff

原意为,线程1,2为进入一段代码,需要先看对方状态是不是true。这段代码在顺序一致模型中没有问题,但在SMP架构中,线程1的先写后读可能在线程2看来已经反序了,如下:

 
Thread 1 Thread 2
reg1 = B

A = true
if (reg1 == false)
    critical-stuff


B = true
reg2 = A

if (reg2 == false)
    critical-stuff

这时,两个线程同时进入了临界代码区。

 
探究原因就必须了解些CPU缓存。
CPU cache用于在CPU与主内存之间缓存数据,按离CPU从近到远分为L1,L2,L3级。L1级可以达到10-100倍的内存访问速度。
CPU cache分write-through与write-back两种,前者写到缓存后,直接触发从缓存往内存的写;后者会等到如缓存满才写主内存。写完缓存后,CPU会执行下条指令,有可能接下来若干条指令执行时,之前所写的内容都仍没有到主内存中。
上例中,写内存A的操作可能写到了缓存但未到主内存,而线程1继续执行了读B。而对线程2来说,A在主内存并没有变化,因此从线程2的角度,线程1的实际执行顺序“反序”了。
 
 
多核情况下,各核有自己的缓存会导致内存访问不具备时效性,CPU架构上的“缓存一致性模型”定义各核之间数据的共享机制。
 
考虑下面这段例子:
Thread 1 Thread 2
A = 41
B = 1 // “A is ready”
loop_until (B == 1)
reg = A

线程2期望等到线程1中B设置后,再去读A。

 
按上面讨论的,X86 SMP架构上,没有问题,对线程2来说,线程1的两个写操作不会反序。但对ARM SMP,就不一样了。对线程1,写写,线程2,读读,都不保证顺序的话,就不会跑出期望的结果。
ARM的这种写写都不能保证顺序的情况是由于缓存读写是按一小块一小块来进行引起的。读写某块缓存时,是按块(ARM 32字节)进行,可能会将目标地址附近的数据一起刷新掉,可能比这些数据更老的内存数据仍然没有更新到缓存,这就是不能保证顺序的原因。
 
 
正确使CPU确保有序的方式是内存屏障,而一般锁都会进行内存屏障操作。 

内存屏障告诉CPU内存访问需要确保有序。对于单CPU的X86,其实不需要,它天生支持顺序一致。

 
Thread 1 Thread 2
A = 41
store/store barrier
B = 1 // “A is ready”
loop_until (B == 1)
load/load barrier
reg = A

以上加入了两个内存屏障,写写与读读。

写写屏障的作用是将所有CPU cache中的内容刷入主内存,使后续的写在其他核看来是有序的;读读屏障的作用是将CPU cache中内容清空,保证下次读时是从内存获取数据。
 
以下是读写屏障。
Thread 1 Thread 2
reg = A
load/store barrier
B = 1 // “I have latched A”
loop_until (B == 1)
load/store barrier
A = 41 // update A

如果没有这个屏障,可能线程1在读A时,要从主内存读,同时B有缓存流水线,从主内存读在处理时,B=1已经写了,并且通过一些CPU cache一致性的方法被线程2读到,而这时内存A仍然在读。

 
对于线程2,由于有个循环,看起来只要CPU不主动将指令执行顺序打乱,是不会在读B前取到A的。但是对于可能存在的第3个线程,可能看到的是A=41,B=0。因为线程2看到的B线程3不一定能看到。所以保险起见,还是加上内存屏障。
 
一开始提过,在X86 SMP中,只需要写读屏障。
各不同CPU有不同的屏障指令,如mfence是X86上的全屏障指令。要注意的是,内存屏障保证的只是访问顺序,不能把它当作CPU cache的flush机制来使用。
 
do {
success = atomic_cas(&lock, 0, 1) // acquire
} while (!success)
full_memory_barrier() critical-section full_memory_barrier()
atomic_store(&lock, 0) // release
上述是一个spinlock,用来执行一段关键的代码段。spinlock只在多CPU情况下使用,理想的实现是spin一段时间后转为非spin形式的lock。
这里有个内存屏障,它的作用有两个:
1 是调用CPU的内存屏障指令, 2是告诉编译器,这里的代码不能乱序。如果没有这个内存屏障调用,编译器可能把代码顺序优化的面目全非。
 
在释放锁前又调用了另一个内存屏障,保证关键代码处的改动对其他CPU可见。
 
实践方面:
C/C++ volatile
在单CPU单线程情况下,十分有用。它防止编译器省略或者将代码乱序,再加上单CPU的顺序一致性,可以保证代码按源码中的顺序执行。
而在单CPU多线程情况下,volatile的内存访问顺序可能会被非volatile打乱,可能需要显式加上编译乱序的barrier;
在SMP情况下,volatile就完全无用。应该被换掉。
 
在C/C++中,volatile往往意味着并发问题。
可以直接用pthread的mutex解决,它内部提供了内存屏障。或者直接用原子操作实现无锁化,这一般很难。
C++将会引入内存屏障相关的操作。
 
总结:
1. volatile在C系代码中,只应出现在开头的嵌入式编程的场景下,其他情况下应杜绝使用;
2. 并发编程加锁是正确操作,内部实现了内存屏障;
3. CPU乱序的原因是多CPU间的缓存同步机制问题;
4. C++后续会在语言层面引入内存屏障。
 
 

误用的volatile的更多相关文章

  1. 谈谈volatile关键字以及常见的误解

    转载请保留以下声明 作者:赵宗晟 出处:https://www.cnblogs.com/zhao-zongsheng/p/9092520.html 近期看到C++标准中对volatile关键字的定义, ...

  2. C++11原子操作与无锁编程(转)

    不讲语言特性,只从工程角度出发,个人觉得C++标准委员会在C++11中对多线程库的引入是有史以来做得最人道的一件事:今天我将就C++11多线程中的atomic原子操作展开讨论:比较互斥锁,自旋锁(sp ...

  3. java中volatile关键字

    一.前言 JMM提供了volatile变量定义.final.synchronized块来保证可见性. 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值.volatil ...

  4. java中volatile关键字的含义

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  5. volatile关键字并不能作为线程计数器

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  6. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  7. 转:java中volatile关键字的含义

    转:java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  8. volatile之一--volatile不能保证原子性

    Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这 ...

  9. java中volatile关键字的含义 (转载)

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

随机推荐

  1. UI测试测试分析

    解析:jQuery easyUI是基于jQuery框架在使用之前应该先引入jquery框架否则jQuery easyUI 将失效故D错误 解析: JQuery UI下的menu插件的使用,menu提供 ...

  2. word-wrap: break-word;和word-break: break-all;的区别

    详细查看以下链接.(转载自张鑫旭大神空间) http://www.zhangxinxu.com/wordpress/2015/11/diff-word-break-break-all-word-wra ...

  3. Ecshop:后台添加新功能栏目以及管理权限设置

    一.添加菜单项 打开 /admin/includes/inc_menu.php文件(后台框架左边菜单),在最后添加一行如下: $modules['17_other_menu']['sns_list'] ...

  4. 【iCore3双核心板】发布 iCore3 应用开发平台硬件原理图

     原理图PDF下载地址:http://pan.baidu.com/s/1jHY0hNK iCore3应用开发平台购买地址:https://item.taobao.com/item.htm?spm=a1 ...

  5. C语言3

    C语言的学习已经进入尾声,再过几天就要考试了,今天我们用C语言做了一个推箱子的游戏.就相当于复习以前的知识啦,但是感觉好难啊,但是老师教我们用函数的思想,让我们"分",把问题分解开 ...

  6. 20145220&20145209&20145309信息安全系统设计基础实验报告(3)

    20145220&20145209&20145309信息安全系统设计基础实验报告(3) 实验报告链接: http://www.cnblogs.com/zym0728/p/6132243 ...

  7. linux安装jdk和scala

    安装jdk 1.下载jdk 2.在linux中创建一个文件夹java,我习惯放在user下 3.上传jdk安装包到java下,然后解压 4.ect/profile下修改文件,添加环境变量 JAVA_H ...

  8. node静态资源管理变迁之路

    使用express自带的,express.static,如:app.use(express.static('hehe')),就可以用localhost/hua.png,访问项目根目录下,hehe文件夹 ...

  9. chm转换为html

    在Windows下chm转换为html的超简单方法(反编译CHM文件的方法) 通过调用Windows命令,将chm 文件转换为html 文件. 方法: 命令行(cmd),输入hh -decompile ...

  10. 接入WebSocket记录

    为什么用 WebSocket 因为APP里面有个聊天功能,需要服务器主动推数据到APP.HTTP 通信方式只能由客户端主动拉取,服务器不能主动推给客户端,如果有实时的消息,要立刻通知客户端就麻烦了,要 ...