多处理器使用松散的内存模型可能会非常混乱,写操作可能会无序,读操作可能会返回不是我们想要的值,为了解决这些问题,我们需要使用内存栅栏(memory fences),或者说内存屏障(memory barrier)。

  X86平台可能还算是使用松散内存模型的多处理器中还算比较好的了,它针对内存顺序有一定的规范要求,原文如下:

Loads are not reordered with other loads.
Stores are not reordered with other stores.
Stores are not reordered with older loads.
In a multiprocessor system, memory ordering obeys causality (memory ordering respects transitive visibility).
In a multiprocessor system, stores to the same location have a total order.
In a multiprocessor system, locked instructions have a total order.
Loads and stores are not reordered with locked instructions.

  其中Load是载入的意思,Store是存储的意思,我们可以简单的对应read和write。这些规则主要的意思就是,Load操作不会和其他Load操作进行操作重排,Store操作不会和其他Store操作进行重排,Store操作不会和之前的Load操作进行重排,使用锁指令的时候不会进行操作重排,其中有些意思我也不是理解的非常透彻,大家自己理解。

  同时X86平台也提供了内存屏障的操作指令,例如mfence,lfence和sfence。但是使用内存屏障指令会导致执行效率的下降,因为CPU需要循环等待。

  这里有个重要的问题,你既不希望编译器针对你的代码加上栅栏导致运行效率过低,同时也不希望它给你生成不正确的代码。

  在X86平台上,有一条重要的事项没有保障,就是“Load操作可能在不同的位置上面比它之前的Store操作更早的执行”,这里的位置是内存位置,比如我们简单的认为是一个变量。

  有一个例子可以说明这个问题,比如x和y是在共享内存中的,他们被初始化为0,r1,r2是处理器的2个寄存器。

Thread 0 Thread 1
mov [_x], 1 mov [_y], 1
mov r1, [_y] mov r2, [_x]

  当Thread0, 和1这两个线程同时在不同的CPU上被执行,就可能会发生r1, r2的值为0的情况。注意发生这种情况的原因就是因为CPU在Store操作之前先执行了Load操作。

  发生这种事情会有什么影响呢?那我们举一个多线程锁的特殊例子,Peterson lock,它是一个针对2个线程互斥的特殊算法。它假设2个线程都有自己的线程ID,不是0,就是1,也就是说其中一个线程ID为0,一个为1.他的实现代码是这样的。

class Peterson
{
private:
// indexed by thread ID, 0 or 1
bool _interested[];
// who's yielding priority?
int _victim;
public:
Peterson()
{
_victim = ;
_interested[] = false;
_interested[] = false;
}
void lock()
{
// threadID is thread local,
// initialized either to zero or one
int me = threadID;
int he = - me;
_interested[me] = true;
_victim = me;
while (_interested[he] && _victim == me)
continue;
}
void unlock()
{
int me = threadID;
_interested[me] = false;
}
}

  为了解释这个算法是如何工作的,让我们假设一下,我们就是其中的一个线程。当我(就是那个线程)需要获得锁的时候,我先将我感兴趣的_interested slot置为true,只有我能写这个slot,其他的线程只能读。同时我将_victim(牺牲者)置为我自己的ID。然后我检查另外的一个线程是否已经获得了锁,如果另外的线程已经获取到了锁,那么我就Spin,也就是循环等待。但是一旦另外的线程释放了锁,并且将它自己设置为_victim,那我就可以获取到锁了,然后就可以针对临界区的数据进行操作了。

  如果已经基本理解了这个算法的意思,我们将问题简化一下,只抽取其中最关键部分的代码,将_interested数组中的2个slot,使用2个变量zeroWants和oneWants来替代。

  他们初始化为:

zeroWants = false;
oneWants = false;
victim = ;

  考虑2个线程同时操作时,我们列出下面的表格:

Thread 0 Thread 1
zeroWants = true;
victim = 0;
while (oneWants && victim == 0)
     continue;
// critical code
zeroWants = false;
oneWants = true;
victim = 1;
while (zeroWants && victim == 1)
      continue;
// critical code
oneWants = false;

  最后,我们再将上面的表格转换成伪汇编代码

Thread 0 Thread 1
store(zeroWants, 1)
store(victim, 0)
r0 = load(oneWants)
r1 = load(victim)
store(oneWants, 1)
store(victim, 1)
r0 = load(zeroWants)
r1 = load(victim)

  先我们来看下Load和Store zeroWants和oneWants2个变量,在上面的表格是我们期待的正确Load和Store顺序。但是处理器是允许先Load oneWants,然后再Store zeroWants的,同样也允许先Load zeroWants,然后再Store oneWants的。那么乱序执行的样子就可能就是下表这个样子:

Thread 0 Thread 1
r0 = load(oneWants)
store(zeroWants, 1)
store(victim, 0)
r1 = load(victim)
r0 = load(zeroWants)
store(oneWants, 1)
store(victim, 1)
r1 = load(victim)

  如果这样执行的话,由于zeroWants和oneWants初始化为0,那么r0, r1也就会变成0,那么spin loop(循环等待)就永远不会被执行,2个线程同时可以针对临界区的数据进行操作! Peterson lock 在X86平台上就失效了!

  当然是有办法解决这个问题的,那么就是使用fence类的指令来强制按照正确的顺序执行。在同一个线程中,fence指令可以加在任何store zeroWants和load oneWants之间,在另外一个线程中情况也是一样。这样就能保证正确的执行顺序了。

  当然我们在平时的编程过程中,并不需要过多的考虑乱序执行以及内存屏障,我们的操作系统比如linux,在它实现加锁的时候,已经将内存屏障的指令加入进去了。

X86平台乱序执行简要分析(翻译为主)的更多相关文章

  1. 【UEFI】---史上最全的X86平台启动流程分析(软硬结合)

    最近研究了下X86处理器的启动流程分析,相比于常见的ARM来说,X86平台启动流程真的复杂了很多,本次基于项目实际的两个问题入手,研究了包括以下几个部分的内容: 1. 从硬件角度看启动流程 2. 从软 ...

  2. 简要分析武汉一起好P2P平台的核心功能

    写作背景 加入武汉一起好,正式工作40天了,对公司的核心业务有了更多的了解,想梳理下自己对于P2P平台的认识. 武汉一起好,自己运营的yiqihao.com,是用PHP实现的,同时也帮助若干P2P平台 ...

  3. 英语学习/词典app行业top5简要分析

    综述 根据豌豆夹上的下载量如下图所示,我们组确定的国内行业top5分别是:有道词典(黄明帅负责),金山词霸(黄珂锐负责),百度翻译(刘馨负责),百词斩(贾猛负责),英语流利说(亢建强负责)(时间有限, ...

  4. ITS简要分析流程(using Qiime)

    Qiime安装 参考资料:http://blog.sina.com.cn/s/blog_83f77c940101h2rp.html Qiime script官方说明http://qiime.org/s ...

  5. Android Hal层简要分析

    Android Hal层简要分析 Android Hal层(即 Hardware Abstraction Layer)是Google开发的Android系统里上层应用对底层硬件操作屏蔽的一个软件层次, ...

  6. RxJava && Agera 从源码简要分析基本调用流程(2)

    版权声明:本文由晋中望原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/124 来源:腾云阁 https://www.qclo ...

  7. 面向英特尔® x86 平台的 Unity* 优化指南: 第 1 部分

    原文地址 目录 工具 Unity 分析器 GPA 系统分析器 GPA 帧分析器 如要充分发挥 x86 平台的作用,您可以在项目中进行多种性能优化,以最大限度地提升性能. 在本指南中,我们将展示 Uni ...

  8. hidraw设备简要分析

    关键词:hid.hidraw.usbhid.hidp等等. 下面首先介绍hidraw设备主要用途,然后简要分析hidraw设备驱动(但是不涉及到相关USB/Bluwtooth驱动),最后分析用户空间接 ...

  9. 基于X86平台的PC机通过网络发送一个int(32位)整数的字节顺序

    1.字节顺序 字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端.大端两种字节顺序.小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处:大端字节序是高字节数据存 ...

随机推荐

  1. BZOJ1066 [SCOI2007]蜥蜴

    首先...这是道(很水的)网络流 我们发现"每个时刻不能有两个蜥蜴在同一个柱子上"这个条件是没有用的因为可以让外面的先跳,再让里面的往外跳 但是还有柱子高度的限制,于是把柱子拆点为 ...

  2. 将windows下的PLSQL转移到Ubuntu上

    1,首先下载安装wine,安装不成功的更新下源即可. 2,Ctal+Alt+T 打开控制台: cd ~/.wine/drive_c mkdir -p oracle/bin mkdir -p oracl ...

  3. 重点关注之自定义序列化方式(Protobuf和Msgpack)

    除了默认的JSON和XML序列化器外,如果想使用其它格式的(比如二进制)序列化器,也是可以的.比如著名的Protobuf和Msgpack,它们都是二进制的序列化器,特点是速度快,体积小.使用方法如下. ...

  4. oracle遇到死锁杀进程

    java程序操作数据库的时候,遇到死锁:java.sql.SQLException: ORA-00060: 等待资源时检测到死锁 解决步骤: 1.找到死锁的oralce对象(表): select ob ...

  5. hdu 4609 3-idiots

    http://acm.hdu.edu.cn/showproblem.php?pid=4609 FFT  不会 找了个模板 代码: #include <iostream> #include ...

  6. Elasticsearch内存分配设置详解

    Elasticsearch默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了.如果你正在使用这个默认堆内存配置,你的集群配置可能会很快发生问题. 这里有两种方式修改Elastic ...

  7. visual studio 2013连接Oracle 11g并获取数据:(二:实现)

    1.VS中新建一个winform窗体 (1)一个按钮 (2)一个数据表格视图(在里面显示得到的数据表) 2.双击按钮进入代码 (1)添加 using System.Data.OracleClient; ...

  8. html,body最顶层元素.

    1,元素百比分是相对父元素,所有元素默认父元素是body. absolute,fixed[只有一个父元素,浏览器窗口]除外[浏览器窗口,为父元素].css3:vh,vw也永远相对,浏览器窗口.heig ...

  9. win10环境下使用苹果虚拟机不要开多线程应用下载文件

    win10环境下使用苹果虚拟机开多线程应用下载文件时候卡死,网络老掉. 8GB内存不够用?2.5mb网速不够用? 开的百度网盘下载个电影 结果虚拟机卡的不行 关了 网盘 挂起虚拟机 然后再 继续运行客 ...

  10. @ResultMapping注解

    @RequestMapping注解1.url映射放在方法上:@RequestMapping("/itemsEdit")2.窄化url请求映射放在类上,定义根路径,url就变成根路径 ...