内存屏障

原文地址  作者:Martin Thompson  译者:一粟   校对:无叶,方腾飞

本文我将和大家讨论并发编程中最基础的一项技术:内存屏障或内存栅栏,也就是让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术。

CPU使用了很多优化技术来实现一个目标:CPU执行单元的速度要远超主存访问速度。在上一篇文章 “Write Combing (合并写)”中我已经介绍了其中的一项技术。CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大化利用缓存,从而把因为缓存未命中引起的延迟降到最小。

当一个程序执行时,只要最终的结果是一样的,指令是否被重排并不重要。例如,在一个循环里,如果循环体内没用到这个计数器,循环的计数器什么时候更新(在循环开始,中间还是最后)并不重要。编译器和CPU可以自由的重排指令以最佳的利用CPU,只要下一次循环前更新该计数器即可。并且在循环执行中,这个变量可能一直存在寄存器上,并没有被推到缓存或主存,这样这个变量对其他CPU来说一直都是不可见的。

CPU核内部包含了多个执行单元。例如,现代Intel CPU包含了6个执行单元,可以组合进行算术运算,逻辑条件判断及内存操作。每个执行单元可以执行上述任务的某种组合。这些执行单元是并行执行的,这样指令也就是在并行执行。但如果站在另一个CPU角度看,这也就产生了程序顺序的另一种不确定性。

最后,当一个缓存失效发生时,现代CPU可以先假设一个内存载入的值并根据这个假设值继续执行,直到内存载入返回确切的值。

1 CPU核
2 |
3 V
4 寄存器
5 |
6 V
7 执行单元 -> Load/Store缓冲区->L1 Cache --->L3 Cache-->内存控制器-->主存
8 | |
9 +-> Write Combine缓冲区->L2 Cache ---+

代码顺序并不是真正的执行顺序,只要有空间提高性能,CPU和编译器可以进行各种优化。缓存和主存的读取会利用load, store和write-combining缓冲区来缓冲和重排。这些缓冲区是查找速度很快的关联队列,当一个后来发生的load需要读取上一个store的值,而该值还没有到达缓存,查找是必需的,上图描绘的是一个简化的现代多核CPU,从上图可以看出执行单元可以利用本地寄存器和缓冲区来管理和缓存子系统的交互。

在多线程环境里需要使用某种技术来使程序结果尽快可见。这篇文章里我不会涉及到 Cache Conherence 的概念。请先假定一个事实:一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏

内存屏障提供了两个功能。首先,它们通过确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,而保持程序顺序的外部可见性;其次它们可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统。

大多数的内存屏障都是复杂的话题。在不同的CPU架构上内存屏障的实现非常不一样。相对来说Intel CPU的强内存模型比DEC Alpha的弱复杂内存模型(缓存不仅分层了,还分区了)更简单。因为x86处理器是在多线程编程中最常见的,下面我尽量用x86的架构来阐述。

Store Barrier

Store屏障,是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存。这会使得程序状态对其它CPU可见,这样其它CPU可以根据需要介入。一个实际的好例子是Disruptor中的BatchEventProcessor。当序列Sequence被一个消费者更新时,其它消费者(Consumers)和生产者(Producers)知道该消费者的进度,因此可以采取合适的动作。所以屏障之前发生的内存更新都可见了。

01 private volatile long sequence = RingBuffer.INITIAL_CURSOR_VALUE;
02 // from inside the run() method
03 T event = null;
04 long nextSequence = sequence.get() + 1L;
05 while (running)
06 {
07     try
08     {
09         final long availableSequence = barrier.waitFor(nextSequence);
10         while (nextSequence <= availableSequence)
11         {
12             event = ringBuffer.get(nextSequence);
13             boolean endOfBatch = nextSequence == availableSequence;
14             eventHandler.onEvent(event, nextSequence, endOfBatch);
15             nextSequence++;
16         }
17         sequence.set(nextSequence - 1L);
18         // store barrier inserted here !!!
19     }
20     catch (final Exception ex)
21     {
22         exceptionHandler.handle(ex, nextSequence, event);
23         sequence.set(nextSequence);
24         // store barrier inserted here !!!
25         nextSequence++;
26     }
27 }

Load Barrier

Load屏障,是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。这使得从其它CPU暴露出来的程序状态对该CPU可见,这之后CPU可以进行后续处理。一个好例子是上面的BatchEventProcessor的sequence对象是放在屏障后被生产者或消费者使用。

Full Barrier

Full屏障,是x86上的”mfence“指令,复合了load和save屏障的功能。

Java内存模型

Java内存模型volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。

原子指令和Software Locks

原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。

内存屏障的性能影响

内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器可以按单元执行任务,然后在任务单元的边界放上所有需要的内存屏障。采用这个方法可以让处理器不受限的执行一个任务单元。合理的内存屏障组合还有一个好处是:缓冲区在第一次被刷后开销会减少,因为再填充改缓冲区不需要额外工作了。 

时间: 2024-09-19 10:45:42

内存屏障的相关文章

内存屏障与JVM并发

内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限 制.本文介绍了内存屏障对多线程程序的影响.我们将研究内存屏障与JVM并发机 制的关系,如易变量(volatile).同步(synchronized)和原子条件式 (atomic conditional).本文假定读者已经充分掌握了相关概念和Java内存模 型,不讨论并发互斥.并行机制和原子性.内存屏障用来实现并发编程中称为可 见性(visibility)的同样重要的作用. 内存屏障为何重要? 对主存的一次访问一般花费硬件的数百

内存屏障机制及内核相关源代码分析

1.--->ymons 在www.linuxforum.net Linux内核技术论坛发贴问:set_current_state和__set_current_state的区别? #define __set_current_state(state_value) /do { current->state = (state_value); } while (0) #define set_current_state(state_value) /set_mb(current->state, (st

linux内存屏障浅析

内存屏障是一个很神奇的东西,之前翻译了linux内核文档memory-barriers.txt,对内存屏障有了一定有理解.现在用自己的方式来整理一下. 在我看来,内存屏障主要解决了两个问题:单处理器下的乱序问题和多处理器下的内存同步问题. 为什么会乱序 现在的CPU一般采用流水线来执行指令.一个指令的执行被分成:取指.译码.访存.执行.写回.等若干个阶段.然后,多条指令可以同时存在于流水线中,同时被执行. 指令流水线并不是串行的,并不会因为一个耗时很长的指令在"执行"阶段呆很长时间,而

[译] LINUX内核内存屏障

                         ================= LINUX内核内存屏障 ================= By: David Howells <dhowells@redhat.com> Paul E. McKenney <paulmck@linux.vnet.ibm.com> 译: kouu <kouucocu@126.com> 出处: Linux内核文档 -- Documentation/memory-barriers.txt

剖析Disruptor:为什么会这么快?(三)揭秘内存屏障

最近我博客文章更新有点慢,因为我在忙着写一篇介绍内存屏障(Memory Barries)以及如何将其应用于Disruptor的文章.问题是,无论我翻阅了多少资料,向耐心的Martin和Mike请教了多少遍,以试图理清一些知识点,可我总是不能直观地抓到重点.大概是因为我不具备深厚的背景知识来帮助我透彻理解. 所以,与其像个傻瓜一样试图去解释一些自己都没完全弄懂的东西,还不如在抽象和大量简化的层次上,把我在该领域所掌握的知识分享给大家 .Martin已经写了一篇文章<going into memor

单处理器中,多进程或多线程之间是否需要使用内存屏障

问题描述 单处理器中,多进程或多线程之间是否需要使用内存屏障 在看Linux内核内存屏障时,文章中写道"只有在存在多CPU交互或CPU与设备交互的情况下才可能需要用到内存屏障.",对此表示怀疑,难道单CPU中,两个进程或两个线程间就不需要用了么? 解决方案 使用LOCK和UNLOCK之后, 一般就不再需要其他内存屏障了(但是注意"MMIO写屏障"章节中所提到的例外). 只有在存在多CPU交互或CPU与设备交互的情况下才可能需要用到内存屏障. 如果可以确保某段代码中不

内核同步机制-优化屏障和内存屏障

优化屏障 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行.然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行. Linux用宏barrier实现优化屏障,gcc编译器的优化屏障宏定义列出如下(在include/linux/compiler-gcc.h中):  #define barrier() __asm__ __volatile_

Linux内核的内存屏障

内容: 抽象的内存访问模型 设备操作 保障 什么是内存屏障? 内存屏障的种类 什么是内存屏障不能确保的? 数据依赖屏障 控制依赖 SMP屏障配对 内存屏障顺序的例子 read内存屏障与load预取 传递性 显式内核屏障 编译屏障 CPU内存屏障 MMIO write屏障 隐式内核内存屏障 锁功能 中断禁用功能 休眠和唤醒功能 其它函数 CPU之间的锁屏障效应 锁与内存访问 锁与I/O访问 什么地方需要内存障碍? 多处理器间的交互 原子操作 设备访问 中断 内核的I/O屏障效应 假想的最小执行顺序

Java内存模型Cookbook(二)内存屏障

原文:http://gee.cs.oswego.edu/dl/jmm/cookbook.html 第二节 作者:Doug Lea 翻译:潘曦   校对:方腾飞 指令重排 内存屏障 多处理器 指南 编译器和处理器必须同时遵守重排规则.由于单核处理器能确保与"顺序执行"相同的一致性,所以在单核处理器上并不需要专门做什么处理,就可以保证正确的执行顺序.但在多核处理器上通常需要使用内存屏障指令来确保这种一致性.即使编译器优化掉了一个字段访问(例如,因为一个读入的值未被使用),这种情况下还是需要