在嵌入式编程中,有对某地址重复读取两次的操作,如地址映射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. odoo 中X2many类型的视图继承

    我们知道视图的继承可以使用inherit_id,但是对于诸如one2many类型的字段,如何利用xpath继承修改其视图呢? 问题:如果直接写one2many类型的字段,会报不存在该字段的错误: 原视 ...

  2. js事件机制——事件冒泡和捕获

    概念:当给子元素和父元素定义了相同的事件,比如都定义了onclick事件,点击子元素时,父元素的onclick事件也会被触发.js里称这种事件连续发生的机制为事件冒泡或者事件捕获. IE浏览器:事件从 ...

  3. Unity3D "Library\UnityAssemblies\UnityEngine.xml" is denied错误解决方法

    错误信息 Access to the path "Library\UnityAssemblies\UnityEngine.xml" is denied 无法修改改文件 Unity版 ...

  4. 一、jquery简介

    认识jquery jquery是有美国人John Resig于2006年创建的一个开元项目,随着被人们的熟知,越来越多的程序高手加入其中,完善和壮大其项目内容:如今已开展成为集javascript.c ...

  5. iOS CoreAnimation 核心动画

    一 介绍 一组非常强大的动画处理API 直接作用在CALAyer上,并非UIView(UIView动画) CoreAnimation是所有动画的父类,但是不能直接使用,应该使用其子类 属性: dura ...

  6. myeclipse项目上出现红色叹号

    右键选中项目:build path→configure build path (由于的我是在问题解决之后发表的博客,所以jar包上面的红色叉子不见了,只要选中红色的jar包,然后选择‘Remove’按 ...

  7. soui使用wke时,设置js回调注意事项

    wke响应网页js函数调用时注意: 必须等网页加载完成后,才能通过SetJsFunc设置js函数与c++回调的对应.网页未加载就设置,不会响应c++函数. 示例代码: wkeJSData* data ...

  8. python 2.43 升级到2.7

    [root@GW1 bin]# lsb_release -aLSB Version: :core-3.1-amd64:core-3.1-ia32:core-3.1-noarch:graphics-3. ...

  9. linux建立文件夹软连接

    linux建立文件夹软连接,并强制覆盖 ln -sfn /home/var/log/httpd/logs logs 这将在当前目录下建立logs软连接,指向/home/var/log/httpd/lo ...

  10. ajax返回json在 IE下,提示打开或保存的解决方法

    Content-type要设置成 text/html 我是用的mvc jquery.form.js 提交的表单. 返回json响应数据. 结果在ie下提示打开或保存,查看保存的内容.就是我返回的jso ...