在嵌入式编程中,有对某地址重复读取两次的操作,如地址映射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. java 写一个"HelloJavaWorld你好世界"输出到操作系统文件Hello.txt文件中

    package com.beiwo.homework; import java.io.File; import java.io.FileOutputStream; import java.io.IOE ...

  2. fnc.tld学习编写

    使用 el 的过程中,需要使用到后端代码处理逻辑,这个时候我们就需要自定义 方法. 如我们后端代码定义如下: package com.rhythmk.common; public class FncH ...

  3. 浅谈iOS触摸事件理解

    iOS的触摸事件个人总结,分为两步: 第一步:是找到哪个视图上触摸 第二步:分析由谁去响应(响应者连) 1.寻找被触摸的视图原理如下图 hitText:withEvent:的方法处理流程: 首先会在当 ...

  4. C语言末

    最后一篇C语言的随笔啦,今天考试,感觉脑袋里一片空白,好像失忆了一样,但是不管怎么说,反正已经考完试了,成绩的好坏只能说明你以前的学习情况,不能预测你下一阶段的学习,不管考的怎么样,都已经过去了,考试 ...

  5. ExtJS笔记 Field

    Fields are used to define what a Model is. They aren't instantiated directly - instead, when we crea ...

  6. Tomcat 随机挂掉

    最近遇到一些诡异的事情,1Apache + 4Tomcat的系统中,每天偶尔会发现其中的一台或几台Tomcat宕机了. 今天特别频繁,宕了有6次之多.    环境: Windows Server 20 ...

  7. 带你玩转JavaWeb开发之四 -如何用JS做登录注册页面校验

    今日内容 使用JQuery完成页面定时弹出广告 使用JQuery完成表格的隔行换色 使用JQuery完成复选框的全选效果 使用JQuery完成省市联动效果 使用JQuery完成下列列表左右选择 使用J ...

  8. Solr Python API : SolrCloudpy 与 Pysolr 的 对比

    http://ae.yyuap.com/pages/viewpage.action?pageId=920314 SolrCloudpy文档:http://solrcloudpy.github.io/s ...

  9. vert.x学习(三),Web开发之Thymeleaf模板的使用

    在vert.x中使用Thymeleaf模板,需要引入vertx-web-templ-thymeleaf依赖.pom.xml文件如下 <?xml version="1.0" e ...

  10. CXF Spring开发WebService,基于SOAP和REST方式 【转】

    官网示例 http://cxf.apache.org/docs/writing-a-service-with-spring.html http://cxf.apache.org/docs/jax-rs ...