背景

同步基元分为用户模式和内核模式

用户模式:Iterlocked.Exchange(互锁)、SpinLocked(自旋锁)、易变构造(volatile关键字、volatile类、Thread.VolatitleRead|Thread.VolatitleWrite)、MemoryBarrier。

内存屏障(英語:Memory barrier),也称内存栅栏内存栅障屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内存屏障之前的指令和之后的指令不会由于系统优化等原因而导致乱序。

大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

内容来源:维基百科

C#的内存屏障操作有哪些

Thread.MemoryBarrier、Volatile 变量、volatile 类、InterLocked 。这个结论是clr via C#4中可以得到答案P678\P677

MESI优化带来的可见性问题

MESI协议,也就是缓存一致性协议。这个协议存在一个问题,就是当CPU0修改当前缓存的共享数据时,需要发送一个消息给其他缓存了相同数据的CPU核心,这个消息传递给其他CPU核心以及收到消息完成各自缓存状态的切换这个过程中,CPU会等待所有缓存响应完成,这样会降低处理器的性能。为了解决这个问题,引入了 StoreBufferes存储缓存。


处理器把需要写入到主内存中的值先写入到存储缓存中,然后继续去处理其他指令。当所有的CPU核心返回了失效确认时,数据才会被最终提交。但是这种优化又会带来另外的问题。 如果某个CPU尝试将其他CPU占有的共享数据写入到内存,消息提交给store buffer以后,当前CPU继续做其他事情,而如果后面的指令依赖于这个被写入内存的最新数据(由于store buffer还没有写入到内存),就会产生可见性问题(也就是值还没有更新到内存中,这个时候读取到的共享数据的值是错误的)。

Store Bufferes带来的CPU内存的乱序访问导致的可见性问题

Store Bufferes中的数据何时写入到内存中是不确定的,那么意味着这个过程的执行顺序也是不确定的,比如下面这个例子 exeToCPU0和exeToCPU1分别在两个独立的cpu核心上执行,假如CPU0 缓存了 isFinish这个共享变量,并且状态为(E->独占),而value可能是(S共享状态被其他CPU核心修改以后变为I(失效状态)。 这种情况下value的缓存数据变更路径为, value将失效状态需要响应给触发缓存更新的CPU核心,接着该CPU将 StoreBufferes写入到内存,这就会导致value会比isFinish更迟的抛弃存储缓存。那么就可能出现CPU1读取到了isFinish的值为true,而value的值不等于10的情况。 这种CPU的内存乱序访问,会带来可见性问题。

value = 3;

void exeToCPU0(){

 value = 10;

 isFinsh = true;

}

void exeToCPU1(){

 if(isFinsh){

   assert value == 10;

 }

}

从硬件层面很难去知道软件层面上的这种前后依赖关系,所以没有办法通过某种手段自动去解决,因此CPU层面提供了 memory barrier(内存屏障)的指令,从硬件层面来看这个 memroy barrier就是CPU flush store bufferes中的指令。软件层面可以决定在适当的地方来插入内存屏障。

CPU层面的内存屏障

什么是内存屏障?从前面的内容基本能有一个初步的猜想,内存屏障就是将 store bufferes中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。 X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)

  • Store Memory Barrier(写屏障) 告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的

  • Load Memory Barrier(读屏障) 处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的

  • Full Memory Barrier(全屏障) 确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作

有了内存屏障以后,对于上面这个例子,我们可以这么来改,从而避免出现可见性问题

value = 3;

void exeToCPU0(){

 value = 10;

 storeMemoryBarrier(); //这个是一个伪代码,插入一个写屏障,使得value=10这个值强制写入到主内存中

 isFinsh = true;

}

void exeToCPU1(){

 if(isFinsh){

   loadMemoryBarrier();//伪代码,插入一个读屏障,使得cpu1从主内存中获得最新的数据

   assert value == 10;

 }

}

总的来说,内存屏障的作用可以通过防止CPU对内存的乱序访问来保证共享数据在多线程并行执行下的可见性

总结:内存屏障 :从硬件层面来看这个 memroy barrier就是CPU flush store bufferes中的指令。内存屏障就是将 store bufferes中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。MemoryBarrier 解决Store Bufferes带来的CPU内存的乱序访问导致的可见性问题

内存屏障类型类型

  • 全内存屏障(mfence):barrier之前的load/store操作均比之后的先完成,且前后的指令不能共同参与指令重排序;
  • 读屏障(lfence):barrier之前的load比之后的load先完成;
  • 写屏障(sfence):barrier之前的store比之后的store先完成;

.net内存屏障类型

全屏障:Thread.MemoryBarrier()\Interlocked.MemoryBarrier(),编译器或cpu在给代码做优化时,不允许代码上下流动(以全屏障代码为基准)。

读屏障:volatile关键定义的变量,读取时候有读屏障功能。编译器或cpu在给代码做优化时,下面代码不能跑到上面,上面的代码可以跑下来(以读屏障代码为基准)。

写屏障: volatile关键定义的变量,写取时候有写屏障功能。编译器或cpu在给代码做优化时,上面的代码不能跑下来,下面的代码可以跑上去(以写屏障代码为基准)。

内存屏障主要有两个作用:

1、刷新写缓存:Thread.MemoryBarrie就是刷新store bufferes 使得数据同步到内存。
2、 阻止指令重排:编译器或clr cpu不能将Thread.MemoryBarrier() 前面代码,移动到他后面,也不允许它后面的代码 移到它前面。它就像一堵墙隔离了代码优化带来的代码移动。

内存屏障 案例:

using System;
using System.Threading;
class Foo
{
int _answer;
bool _complete; void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1刷新Store Bufferes使得_answer = 123这个值强制写入到主内存中
_complete = true;
Thread.MemoryBarrier(); // Barrier 2刷新Store Bufferes使得 _complete=true这个值强制写入到主内存中
} void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine(_answer);
}
}
}

Barrier1 ,Barrier2刷新store bufferes 使得数据同步到内存,保证了其他cpu得到最新的数据。

Barrier3,Barrier4刷新store bufferes保证了_complete、_answer从内存中获取最新值。

“内存模型”和“超标量流水技术”。

C# 我们围绕MemoryBarrier来了解两个概念,“内存模型”和“超标量流水技术”。
内存模型是一个硬件概念,表示机器指令以什么样的顺序被处理器执行。
超标量流水线技术是指处理器中设置了一条以上的流水线,并且每时钟周期内可以并行完成多条指令的执行。
由于这种情况下指令的执行顺序并不是顺序执行的,所以处理器的内存模型分为“强顺序模型(Strong
ordered) 和弱顺序模型(Weak ordered
)”两种。常见的x86架构的处理器采用强顺序内存模型,而移动终端设备大量使用的ARMv7处理器则采用弱顺序内存模型。所以问题就来了,在移动平台上我们就没法保证QueuedWork
=
InQueuedWork;和DoWorkEvent->Trigger();这两句话的执行顺序了。好在对应的平台提供了“内存栅栏”的技术。
内存栅栏是一类同步指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。我们看下FPlatformMisc::MemoryBarrier()在不同平台下的实现:

Android和Linux:

FORCEINLINE static void MemoryBarrier()
{
__sync_synchronize();
}

iOS和Mac:

FORCEINLINE static void MemoryBarrier()
{
OSMemoryBarrier();
}

Windows:

void FGenericPlatformMisc::MemoryBarrier()
{
}

可以看出在ARMv7架构对应的平台都调用了对应的内存栅栏的接口,而基于x86的Windows平台就不需要担心这个问题,所以实现也为空。

如果对硬件架构感兴趣,可以对CISC和RISC做一些了解,计算机组成原理相关的知识,找一找相关的书籍阅读下。

【C# 线程】内存屏障 MemoryBarrier的更多相关文章

  1. [百度空间] [转]内存屏障 - MemoryBarrier

    处理器的乱序和并发执行 目前的高级处理器,为了提高内部逻辑元件的利用率以提高运行速度,通常会采用多指令发射.乱序执行等各种措施.现在普遍使用的一些超标量处理器通常能够在一个指令周期内并发执行多条指令. ...

  2. 高速缓存一致性协议MESI与内存屏障

    一.CPU高速缓存简单介绍 CPU高速缓存机制的引入,主要是为了解决CPU越来越快的运行速度与相对较慢的主存访问速度的矛盾.CPU中的寄存器数量有限,在执行内存寻址指令时,经常需要从内存中读取指令所需 ...

  3. memory barrier 内存屏障 编译器导致的乱序

    小结: 1. 很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误, 2. https://gith ...

  4. 内存屏障 WriteBarrier 垃圾回收 屏障技术

    https://baike.baidu.com/item/内存屏障 内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之 ...

  5. 【C#】通过一个案例 彻底了解 Volatile和 内存屏障

    案例如下的.我个人理解是不会出现出现0,0的结果,但是很明显出现了. 说明对我对 Volatile\内存屏障\乱序排序的理解是不对. 今天就通过这个案例,理清这些概念. using System; u ...

  6. JVM内存模型、指令重排、内存屏障概念解析

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  7. [面试]volatile类型修饰符/内存屏障/处理器缓存

    volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...

  8. synchronized 与 volatile 原理 —— 内存屏障的重要实践

    单例模式的双重校验锁的实现: 第一种: private static Singleton _instance; public static synchronized Singleton getInst ...

  9. JMM中的重排序及内存屏障

    目录 1. 概述 2. 重排序 2-1. as-if-serial语义 2-2. 重排序的种类 2-3. 从Java源代码到最终实际执行的指令序列, 会分别经历下面3中重排序. 3. 内存屏障类型 3 ...

随机推荐

  1. window10教育版激活失败

    问题 输入完key之后显示无法连接服务器 再次输入密钥无效,而且家庭版密钥激活也没了 使用命令行消除过去的key,使用新的教育版key后,显示运行在运行microsoft windows 非核心版本的 ...

  2. 搭服务器之kvm--vnc连接虚拟机连接闪退直接消失 以及virsh shutdown命令无效解决办法。

    之前暑期见识到了虚拟化在企业中的应用,感慨不小,以前只是自己在玩儿桌面vmware workstation,安装的虚拟机也没啥大感觉.在公司机房里大家用的dell poweredge 420,8gme ...

  3. Vulnhub靶机系列之Acid

    Acid 下载地址: ​ https://download.vulnhub.com/acid/Acid.rar ​ https://download.vulnhub.com/acid/Acid.rar ...

  4. python 裴伯拉切数列

    裴伯拉切数列:从第三个元素开始,每个元素为该元素前面的两个元素的和. 裴伯拉切数列:0,1,1,2,3,5,8,13,21,34,55...... 求出小于n的裴伯拉切数列. def fibo(n): ...

  5. CSS实现事件穿透与背景图不跟随滚动条

    1. 事件穿透属性:pointer-events: none  // auto默认值.none:不捕捉target事件(实现穿透) 用途:当需要使用透明遮罩并且允许点击遮罩下方元素时,或需要使用背景容 ...

  6. 2022.02.05 DAY2

    前言 今天陪老姐送对象去安庆了,上午还去了西风禅寺求了个签,第一次拿到中评签,看来今年还需要继续努力哈哈哈.一直到晚上才有时间去做点题目,今天依旧是leetcode. 题目 leetcode 1 两数 ...

  7. cloudcompare备忘录(1)

    1.找点 然后直接在需要的位置上点就会出现这个点的信息了~! 2.想看一个三d的切面时候 先选中切的目标 点击小剪刀~ 点击鼠标左键四次来框选,然后点击鼠标右键确认 再点击这个按钮就切好了

  8. 「JOI 2015 Final」城墙

    「JOI 2015 Final」城墙 复杂度默认\(m=n\) 暴力 对于点\((i,j)\),记录\(ld[i][j]=min(向下延伸的长度,向右延伸的长度)\),\(rd[i][j]=min(向 ...

  9. Android中的多线程【转】

    感谢大佬:https://www.cnblogs.com/zoe-mine/p/7954605.html 感谢大佬:https://blog.csdn.net/u014555121/article/d ...

  10. File 类的 getPath()、getAbsolutePath()、getCanonicalPath() 的区别【转】

    File 类的 getPath().getAbsolutePath().getCanonicalPath() 的区别 感谢大佬:https://blog.csdn.net/zsensei/articl ...