- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
最简单的硬件原语是原子读-改-写(atomic read-modify-write),它极大地简化了同步的实现(如锁(locks)、屏障(barriers)等).
原子交换:交换寄存器和内存的内容。原子交换确保了在执行这一操作时,不会有其他处理器或线程同时访问该内存位置。通过原子交换,可以实现对共享资源的排他性访问.
测试并设置(Test & Set):这是原子交换的一个特殊应用,用于检查内存 Lock 的状态,并将该 Lock 设置为1。操作步骤如下:
首先将内存 Lock 的内容加载到寄存器中.
将该内存 Lock 设置为1.
测试并设置(Test & Set)操作的结果是可以判断某个位置是否已经被占用,通常用于实现锁机制.
如果该 Lock 变量中为 0,则表示当前没有人持有该 Lock,并且没有人正在执行临界区(Critical Section),这样其他人就可以继续获取该 Lock; 。
lock 的实现示例:
lock: t&s register, location
bnz register, lock ;检查寄存器的内容,如果不为零,
;则意味着锁已经被其他进程占用,当前进程需要继续等待。
CS ;表示临界区(Critical Section),即访问共享资源的代码段
;只有获得锁的进程可以进入该区域。
st location, #0 ;退出临界区后,将内存位置重置为0,释放锁。
假设一开始 Lock 未被获取,即它是空闲的,其他线程可以自由地进入这个临界区(Critical Section).
在传统的自旋锁机制中,如果锁存在于内存中,每次获取锁的尝试都会导致总线上产生大量通信流量(因为每次操作都需要访问内存)。这会造成总线拥堵,导致其他进程难以进行有效的进展.
通过引入缓存锁,可以解决这个问题。具体来说:
在多处理器系统中,锁机制通常用于控制对共享资源的访问。为了减少不必要的一致性流量,锁的一致性协议会采取一定措施,尽量减少在等待锁时的通信开销。即test & test & set.
这种机制减少了不必要的一致性流量,使得锁的管理更加高效,同时确保了对共享资源的访问控制.
lock: test register, location
bnz register, lock
t&s register, location
bnz register, lock
CS
st location, #0
Load-Linked(LL)和 Store Conditional(SC)是一种用于实现原子操作的机制,通常在并发编程中用于实现线程安全的操作。与其他锁机制相比,LL-SC 提供了更灵活且低开销的原子操作方式.
Load-Linked (LL):LL 操作读取一个内存地址的值,并在内部的某个表中记录这个地址。此后,处理器可以进行一些计算,而不立即锁定或更新这个地址.
Store Conditional (SC):SC 操作试图将值写入与 LL 操作相同的内存地址,但只有在该地址自 LL 操作以来没有被其他进程或线程修改过时,SC 操作才会成功。这种机制确保了在没有其他干扰的情况下,该 LL-SC 操作可以被视为原子的.
LL 读取 x 存入 R1,并将 X 地址放入一个表中,该表监视其他进程是否修改过 x,对 R1 进行一系列操作后,SC R1 至 x,若 x 没有被修给过则 SC 成功;若 x 被修改过,则 SC 失败,并在 R1 中设置一个 tag。在该操作完成后检查 R1,若 R1 没有问题,则命令完成,若 R1 被设置了 tag,则重新执行该命令.
在实现中,SC 操作在失败时不会产生总线流量(不需要进行额外的数据传输),这样减少了缓存一致性协议带来的通信开销。因此,LL-SC 的硬件实现比传统的 test&test&set 更加高效,因为后者在失败时需要不断地重试并产生大量总线流量.
总结来说,LL-SC 机制既允许较高的并发度,又降低了硬件和通信开销,因此常用于需要高效原子操作的并行系统中.
lockit: LL R2, 0(R1) ;加载链接指令(Load Linked),将R1+0处的数据加载到寄存器R2中
;这条指令不会引发缓存一致性流量,意味着它不会使缓存内容无效化。
BNEZ R2, lockit ;当锁不可用时,程序会一直“自旋(spin)”在这里
DADDUI R2, R0, #1 ;put value R0+1 in R2
SC R2, 0(R1) ;store-conditional succeeds if no one
;updated the lock since the last LL
BEQZ R2, lockit ;confirm that SC succeeded, else keep trying
i
次读未命中(read-miss)请求:所有等待的进程都会尝试读取锁的状态,这会导致 i
次读未命中请求。i
次(或 1 次)响应:缓存系统返回 i
次读未命中请求的响应,可能是每个请求都有响应,也可能是只需要一次响应就能满足所有等待者。SC
失败:假设在一次 SC
成功之前没有其他进程更改锁状态,则 SC
只需一次即可成功。失败的 SC
计数为零。i-1
次读未命中请求:剩余的 i-1
个进程将继续尝试获取锁状态,从而发出 i-1
次读未命中请求。i-1
次(或 1 次)响应:与之前类似,系统可能为每个读未命中提供独立的响应,也可能通过一次响应满足所有请求。优化点:i 和 i-1 的读未命中请求及其响应可通过缓存机制减少到 1 次,以进一步降低总线事务的数量.
LL-SC
指令完成)。获取票据后,进程会不断检查 now-serving
变量,看看是否轮到自己执行。执行完成后,该进程会将 now-serving
变量递增,允许下一个排队的进程进入。
now-serving
变量,无需反复发送请求,降低了带宽需求。now-serving
变量,而是采用一个 now-serving
数组,每个进程等待属于自己的特定数组元素。
Coherence(缓存一致性):
x
,看到 x <- 5, x <- 7
顺序一致x, y
,看到 x <- 7, y <- 8
顺序不一定一致Consistency(一致性模型):
x, y
,看到 x <- 7, y <- 8
顺序一致Sequential Consistency 。
假设:
有效执行序列(Valid executions):
abAcBCDe...
ABCDEFabGc...
abcAdBe...
aAbBcCdDe...
顺序一致性(Sequential Consistency,SC)是一种内存一致性模型,规定所有处理器对共享内存的读写操作,必须看起来以全局固定的顺序执行,并且这种顺序与各处理器的程序顺序一致.
程序员通常假设程序具有顺序一致性,这使得推理程序行为变得更加简单.
然而,硬件创新可能会破坏这种顺序一致性模型.
例如,如果我们引入写缓冲区、乱序执行,或者在一致性协议中丢弃确认信号(ACKS),那么以前运行正常的程序可能会产生意外的输出.
写缓冲区(Write Buffers):写操作先存入缓冲区,延迟写入内存,可能导致其他处理器看到的是旧值,而非最新值.
乱序执行(Out-of-Order Execution):为了提高性能,处理器可能重新排序指令的执行顺序,导致程序的操作顺序看起来与源码不一致.
丢弃ACKS(Acknowledgment):在缓存一致性协议中,ACK用于确保更新操作被其他处理器确认。如果ACK丢失或被忽略,可能导致不同处理器对内存的视图不一致.
一个乱序执行(Out-of-Order, OOO)核心不会检测到处理 A 的指令和处理 B 的指令之间的依赖性;因此,这些操作可能被重新排序。这种情况对单线程是没有问题的,但在多线程中会引发问题.
说明:一致性模型允许程序员了解硬件可以进行哪些操作重排的假设.
如果一致性协议中的无效操作(Coherence Invalidation)不需要确认信号(ACKs),我们无法确保所有线程都看到 A 的最新值.
就如上图中由于 DIR-A 的 INV 信号传输到 P3 的传输时间太长,导致 P3 执行时没有读取到 A=1.
在有 ACK 信号的传输过程如上所示,P2 的 Req A 信号必须等 P3 的 ACK 信号返回才能执行.
如果一个多处理器系统的执行结果可以通过以下方式实现,则该系统是顺序一致的:
可以通过以下方法实现顺序一致性:
这种模型对于程序员来说非常直观,但执行效率极低.
由于这种方法非常慢,有以下替代方案:
我们希望拥有一种直观的编程模型(例如顺序一致性),同时也希望实现高性能.
在程序的某些部分,我们关心数据竞争(data races)和指令重排(reordering)的限制,而在其他部分则不太关注。 因此,大多数情况下我们会放松顺序一致性的约束,但在代码的特定部分会严格执行这些约束.
内存屏障指令(Fence instructions) 是一种特殊的指令,要求在它之前的所有内存访问必须完成之后,才能继续执行接下来的操作(达到顺序一致性的效果).
定义:Fence 指令是一种显式的同步机制,用于强制某种顺序的内存访问.
功能:
锁机制的问题 。
复杂性:使用锁进行同步时,程序员必须手动管理共享资源,确保每个线程正确加锁和解锁。这容易出错,尤其是在复杂的多线程程序中.
死锁风险:如果线程按照错误的顺序获取多个锁,或者一个线程忘记释放锁,会导致程序无法前进(即死锁).
性能问题:锁是阻塞的,这意味着一个线程持有锁时,其他线程必须等待,从而可能降低并行性.
事务内存的引入 。
事务内存是一种硬件或软件机制,用于替代传统锁同步,简化并行编程:
优势 。
transaction begin
和 transaction end
包裹,无需担心加锁解锁的复杂性。硬件/软件会自动管理事务的冲突检测和回滚。Producer
将任务放入工作队列的尾部(tail
),consumer
从工作队列的头部(head
)拉取任务。head
和 tail
的更新需要加锁,两个线程(Producer
和 consumer
)无法并行进行操作。如下图所示,T1 只改变了 head,T2 只改变了 tail,那么这两个事务可正常执行.
如下图所示,T1 改变了 head 和 tail,T2 只改变了 tail,那么 T2 不能正常执行,事务会回滚(abort)到开始状态,并尝试重新执行.
隔离性 。
事务的隔离性意味着,当事务正在执行时,其他事务或线程无法看到它的中间状态,或者影响它的操作。事务会表现得像是在一个独立的环境中运行,直到它完成后,才将结果提交到共享系统。这种特性使得事务操作对程序员来说更加直观和可靠.
原子性 。
原子性指事务的所有操作(读、写等)要么全部完成,要么全部不完成。这确保了:
如果事务被中途打断(如发生冲突或系统问题),不会留下任何不完整的状态.
事务的结果是要么成功提交所有操作,要么将所有改变回滚到事务开始前的状态.
事务失败和重试 。
如果在事务执行过程中,出现以下问题:
冲突:例如,其他线程同时修改了事务要操作的共享变量.
系统错误:例如,资源不可用或事务无法满足一致性要求.
事务会回滚(abort)到开始状态,并尝试重新执行。这种机制可以自动解决数据冲突,减轻程序员的负担.
transaction begin // 事务开始
read shared variables // 读取共享变量
arithmetic // 执行算术运算或其他逻辑
write shared variables // 修改共享变量
transaction end // 事务结束(如果事务满足条件(如没有冲突),则提交所有操作)
写操作可以缓存(不能直接写入主存):
跟踪读取集合和写入集合:
在其他事务提交时进行比较:
在事务结束时提交意图:
编程简单性(粗粒度锁的优点):
begin
和 end
),无需关心细粒度的同步问题,减少编程复杂性。高性能(细粒度锁的优点):
避免死锁:
数据版本管理(Data Versioning):
冲突检测(Conflict Detection):
适用于基于 Snoop 协议的小型多处理器的实现 。
”Lazy“ 版本管理与 ”Lazy“ 冲突检测 。
不支持事务并行提交 。
当事务发出读请求时:如果数据块尚未在缓存中,则以只读模式(read-only)获取该数据块,并为该缓存行设置 rd-bit(读取位).
当事务发出写请求时:如果数据块尚未在缓存中,则以只读模式获取该数据块,为该缓存行设置 wr-bit(写入位),并在缓存中修改数据.
如果带有 wr-bit 的缓存行被驱逐(evicted),事务必须中止(abort),或者依赖某种软件机制来保存溢出的数据.
当一个事务到达结束时,它需要将其写操作持久化:在事务的最后阶段才提交写操作,使数据持久化.
一个中央仲裁器负责协调(在基于总线的系统中易于实现):
胜出的事务将占用总线,直到所有需要写入的缓存行地址被广播出去(这是事务的提交过程).
无需立即回写到主存,只需将这些缓存行的其他副本无效化即可.
当其他事务(尚未开始提交)检测到它的读集合中的缓存行被无效化时:
”Lazy“ 版本控制:
”Lazy“ 冲突检测:
快速回滚:
提交较慢:
无死锁风险:
饥饿问题:
写操作立即生效:
其他事务读取时返回最新值:
保留旧值以备回滚:
为了防止当前事务因冲突被中止(abort)时丢失旧数据,写操作之前会将旧值保存到一个日志(log)中.
日志存储位置:日志存储在虚拟内存中,可以在缓存中操作,因此性能开销不大.
这种方法被称为 Eager Versioning.
由于事务 A 的写操作立即生效,可能会导致另一个事务 B 的读/写操作出现未命中,并被重定向到事务 A.
在此时,我们检测到冲突(因为两个事务都未到达其结束点,因此这是 ”Eager“ 的冲突检测):两个事务同时操作同一缓存行,并且至少有一个事务进行了写操作.
一种解决方案:请求方停滞。事务 A 向事务 B 发送一个 NACK(否定应答);事务 B 等待并重试,希望事务 A 能够提交并将最新的缓存行交给 B.
可能导致死锁:每个事务都在等待另一个事务完成 。
需要一个独立的(硬件/软件)争用管理器来检测这些死锁并强制其中一个事务中止 。
事务间的读写冲突与停滞:
饿死问题:
日志存储与事务大小:
提交和中止的成本:
最后此篇关于DDCA——内存一致性的文章就讲到这里了,如果你想了解更多关于DDCA——内存一致性的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我在具有 2CPU 和 3.75GB 内存 (https://aws.amazon.com/ec2/instance-types/) 的 c3.large Amazon EC2 ubuntu 机器上运
我想通过用户空间中的mmap-ing并将地址发送到内核空间从用户空间写入VGA内存(视频内存,而不是缓冲区),我将使用pfn remap将这些mmap-ed地址映射到vga内存(我将通过 lspci
在 Mathematica 中,如果你想让一个函数记住它的值,它在语法上是很轻松的。例如,这是标准示例 - 斐波那契: fib[1] = 1 fib[2] = 1 fib[n_]:= fib[n] =
我读到动态内存是在运行时在堆上分配的,而静态内存是在编译时在堆栈上分配的,因为编译器知道在编译时必须分配多少内存。 考虑以下代码: int n; cin>>n; int a[n]; 如果仅在运行期间读
我是 Python 的新手,但我之前还不知道这一点。我在 for 循环中有一个基本程序,它从站点请求数据并将其保存到文本文件但是当我检查我的任务管理器时,我发现内存使用量只增加了?长时间运行时,这对我
我正在设计一组数学函数并在 CPU 和 GPU(使用 CUDA)版本中实现它们。 其中一些函数基于查找表。大多数表占用 4KB,其中一些占用更多。基于查找表的函数接受一个输入,选择查找表的一两个条目,
读入一个文件,内存被动态分配给一个字符串,文件内容将被放置在这里。这是在函数内部完成的,字符串作为 char **str 传递。 使用 gdb 我发现在行 **(str+i) = fgetc(aFil
我需要证实一个理论。我正在学习 JSP/Java。 在查看了一个现有的应用程序(我没有写)之后,我注意到一些我认为导致我们的性能问题的东西。或者至少是其中的一部分。 它是这样工作的: 1)用户打开搜索
n我想使用memoization缓存某些昂贵操作的结果,这样就不会一遍又一遍地计算它们。 两个memoise和 R.cache适合我的需要。但是,我发现缓存在调用之间并不可靠。 这是一个演示我看到的问
我目前正在分析一些 javascript shell 代码。这是该脚本中的一行: function having() { memory = memory; setTimeout("F0
我有一种情况,我想一次查询数据库,然后再将整个数据缓存在内存中。 我得到了内存中 Elasticsearch 的建议,我用谷歌搜索了它是什么,以及如何在自己的 spring boot 应用程序中实现它
我正在研究 Project Euler (http://projecteuler.net/problem=14) 的第 14 题。我正在尝试使用内存功能,以便将给定数字的序列长度保存为部分结果。我正在
所以,我一直在做 Java 内存/注意力游戏作业。我还没有达到我想要的程度,它只完成了一半,但我确实让 GUI 大部分工作了......直到我尝试向我的框架添加单选按钮。我认为问题可能是因为我将 JF
我一直在尝试使用 Flask-Cache 的 memoize 功能来仅返回 statusTS() 的缓存结果,除非在另一个请求中满足特定条件,然后删除缓存。 但它并没有被删除,并且 Jinja 模板仍
我对如何使用 & 运算符来减少内存感到非常困惑。 我可以回答下面的问题吗? clase C{ function B(&$a){ $this->a = &$a; $thi
在编写代码时,我遇到了一个有趣的问题。 我有一个 PersonPOJO,其 name 作为其 String 成员之一及其 getter 和 setter class PersonPOJO { priv
在此代码中 public class Base { int length, breadth, height; Base(int l, int b, int h) { l
Definition Structure padding is the process of aligning data members of the structure in accordance
在 JavaScript Ninja 的 secret 中,作者提出了以下方案,用于在没有闭包的情况下内存函数结果。他们通过利用函数是对象这一事实并在函数上定义一个属性来存储过去调用函数的结果来实现这
我正在尝试找出 map 消耗的 RAM 量。所以,我做了以下事情;- Map cr = crPair.collectAsMap(); // 200+ entries System.out.printl
我是一名优秀的程序员,十分优秀!