首页 > 系统相关 >剖析Disruptor:为什么会这么快?(三)揭秘内存屏障(转)

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

时间:2023-08-04 17:05:30浏览次数:45  
标签:Disruptor 消费者 CPU 屏障 volatile 内存 序列号 揭秘


主题是什么?

我写这个系列的博客主要目的是解析Disruptor是如何工作的,并深入了解下为什么这样工作。理论上,我应该从可能准备使用disruptor的开发人员的角度来写,以便在代码和技术论文[Disruptor-1.0.pdf]之间搭建一座桥梁。这篇文章提及到了内存屏障,我想弄清楚它们到底是什么,以及它们是如何应用于实践中的。

什么是内存屏障?

它是一个CPU指令。没错,又一次,我们在讨论CPU级别的东西,以便获得我们想要的性能(Martin著名的Mechanical Sympathy理论)。基本上,它是这样一条指令: a)确保一些特定操作执行的顺序; b)影响一些数据的可见性(可能是某些指令执行后的结果)。

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。正如去拉斯维加斯旅途中各个站点的先后顺序在你心中都一清二楚。

 

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

内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

和Java有什么关系?

现在我知道你在想什么——这不是汇编程序。它是Java。

这里有个神奇咒语叫volatile(我觉得这个词在Java规范中从未被解释清楚)。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。

 

剖析Disruptor:为什么会这么快?(三)揭秘内存屏障(转)_序列号_02

这意味着如果你对一个volatile字段进行写操作,你必须知道:

1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。

2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

举个例子呗!

很高兴你这样说了。又是时候让我来画几个甜甜圈了。

RingBuffer的指针(cursor)(译注:指向队尾元素)属于一个神奇的volatile变量,同时也是我们能够不用锁操作就能实现Disruptor的原因之一。

 

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

生产者将会取得下一个Entry(或者是一批),并可对它(们)作任意改动, 把它(们)更新为任何想要的值。如你所知,在所有改动都完成后,生产者对ring buffer调用commit方法来更新序列号(译注:把cursor更新为该Entry的序列号)。对volatile字段(cursor)的写操作创建了一个内存屏障,这个屏障将刷新所有缓存里的值(或者至少相应地使得缓存失效)。

这时候,消费者们能获得最新的序列号码(8),并且因为内存屏障保证了它之前执行的指令的顺序,消费者们可以确信生产者对7号Entry所作的改动已经可用。

那么消费者那边会发生什么?

消费者中的序列号是volatile类型的,会被若干个外部对象读取——其他的下游消费者可能在跟踪这个消费者。ProducerBarrier/RingBuffer(取决于你看的是旧的还是新的代码)跟踪它以确保环没有出现重叠(wrap)的情况(译注:为了防止下游的消费者和上游的消费者对同一个Entry竞争消费,导致在环形队列中互相覆盖数据,下游消费者要对上游消费者的消费情况进行跟踪)。

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

所以,如果你的下游消费者(C2)看见前一个消费者(C1)在消费号码为12的Entry,当C2的读取也到了12,它在更新序列号前将可以获得C1对该Entry的所作的更新。

基本来说就是,C1更新序列号前对ring buffer的所有操作(如上图黑色所示),必须先发生,待C2拿到C1更新过的序列号之后,C2才可以为所欲为(如上图蓝色所示)。

对性能的影响

内存屏障作为另一个CPU级的指令,没有锁那样大的开销。内核并没有在多个线程间干涉和调度。但凡事都是有代价的。内存屏障的确是有开销的——编译器/cpu不能重排序指令,导致不可以尽可能地高效利用CPU,另外刷新缓存亦会有开销。所以不要以为用volatile代替锁操作就一点事都没。

你会注意到Disruptor的实现对序列号的读写频率尽量降到最低。对volatile字段的每次读或写都是相对高成本的操作。但是,也应该认识到在批量的情况下可以获得很好的表现。如果你知道不应对序列号频繁读写,那么很合理的想到,先获得一整批Entries,并在更新序列号前处理它们。这个技巧对生产者和消费者都适用。以下的例子来自BatchConsumer:

01	    long nextSequence = sequence + 1;
02	    while (running)
03	    {
04	        try
05	        {
06	            final long availableSequence = consumerBarrier.waitFor(nextSequence);
07	            while (nextSequence <= availableSequence)
08	            {
09	                entry = consumerBarrier.getEntry(nextSequence);
10	                handler.onAvailable(entry);
11	                nextSequence++;
12	            }
13	            handler.onEndOfBatch();
14	            sequence = entry.getSequence();
15	        }
16	        …
17	        catch (final Exception ex)
18	        {
19	            exceptionHandler.handle(ex, entry);
20	            sequence = entry.getSequence();
21	            nextSequence = entry.getSequence() + 1;
22	        }
23	    }
————————————————

(你会注意到,这是个旧式的代码和命名习惯,因为这是摘自我以前的博客文章,我认为如果直接转换为新式的代码和命名习惯会让人有点混乱)

在上面的代码中,我们在消费者处理entries的循环中用一个局部变量(nextSequence)来递增。这表明我们想尽可能地减少对volatile类型的序列号的进行读写。

总结

内存屏障是CPU指令,它允许你对数据什么时候对其他进程可见作出假设。在Java里,你使用volatile关键字来实现内存屏障。使用volatile意味着你不用被迫选择加锁,并且还能让你获得性能的提升。

但是,你需要对你的设计进行一些更细致的思考,特别是你对volatile字段的使用有多频繁,以及对它们的读写有多频繁。

标签:Disruptor,消费者,CPU,屏障,volatile,内存,序列号,揭秘
From: https://blog.51cto.com/u_2650279/6964772

相关文章

  • 剖析Disruptor:为什么会这么快?(三)伪共享(转)
    缓存系统中是以缓存行(cacheline)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在SMP系统中并行线程实现......
  • 如何使用Disruptor(二)如何从Ringbuffer读取(转)
    ConsumerBarrier与消费者这里我要稍微反过来介绍,因为总的来说读取数据这一过程比写数据要容易理解。假设通过一些“魔法”已经把数据写入到RingBuffer了,怎样从RingBuffer读出这些数据呢?(好,我开始后悔使用Paint/Gimp 了。尽管这是个购买绘图板的好借口,如果......
  • 如何使用 Disruptor(三)写入 Ringbuffer(转)
    本文的 重点 是:不要让Ring重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。ProducerBarriersDisruptor 代码给 消费者 提供了一些接口和辅助类,但是没有给写入RingBuffer的 生产者 提供接口。这是因为除了你需要知道生产者之外,没有别人需要访问它。......
  • FX110揭秘资金盘UEZ Markets骗局
    最近,FX110网看见不少关于UEZMarkets外汇券商的投诉,同时,本站也收到了大量UEZMarkets平台投诉。尽管平台网站仍在运营中,已然觉着:UEZMarkets平台是跑路了!在FX110网上搜索UEZMarkets,可以看到UEZMarkets的评分在31.6分,有且只有一个对外宣称持有的美国NFA普通金融牌照,而且还显示“无......
  • 动态内存分配
    动态内存分配头文件#include<stdio.h>#include<stdlib.h>#include<string.h>#include<errno.h>#include<windows.h>mallocintmain(){//像内存申请10个整形的空间int*p=(int*)malloc(10*sizeof(int));//malloc不初始化为零if(p==NULL)......
  • 行行AI9月活动预告:揭秘AI+设计私董会训练营:未来设计的新引擎
    IXDC联合行行AI将于9月中下旬2天时间,在北京举办一场前沿且创新的主题会——《IXDC&行行AI的AI设计私董会训练营》。这是一个集结了人工智能与设计领域的课程内容,旨在通过深度学习和实践,推动AI与设计的融合发展。首先,让我们来理解一下什么是AI+设计。简单来说,就是将人工智能技术应......
  • tflearn 数据集太大无法加载进内存问题?——使用image_preloader 或者是 hdf5 dataset
    tflearn数据集太大无法加载进内存问题?Hi,all!I'mtryingtotraindeepnetonabigdatasetthatdoesn'tfitintomemory.Isthereanywaytousegeneratorstoreadbatchesintomemoryoneverytrainingstep?I'mlookingforbehavioursimilartofit_genera......
  • 【设计模式】享元模式Flyweight:通过共享对象减少内存加载消耗
    (目录)享元模式Flyweight:通过共享对象减少内存加载消耗享元模式的用意享元模式以共享的⽅法⾼效地⽀持⼤量的细粒度对象,享元对象能做到共享的关键是区分内蕴状态和外蕴状态。⼀个内蕴状态是存储在享元对象内部的,并且是不会随环境改变⽽有所不同的,因此⼀个享元可以具有内蕴状态......
  • 【JAVA】Java 内存模型中的 happen-before
    前言Java语言在设计之初就引入了线程的概念,以充分利用现代处理器的计算能力,这既带来了强大、灵活的多线程机制,也带来了线程安全等令人混淆的问题,而Java内存模型(JavaMemoryModel,JMM)为我们提供了一个在纷乱之中达成一致的指导准则。本篇博文的重点是,Java内存模型中的happen-be......
  • 一次JVM内存溢出的排查经过
    文章目录一、背景二、解决办法三、总结一、背景高峰将至,系统访问量进入高峰期。随之系统出现了异常:java.lang.OutOfMemoryError:unabletocreatenewnativethread。在解决这个问题中,尝试了各种方法,最后竟然是因为它…二、解决办法1、关于这个问题,一开始猜想是因消息队列(acti......