addl, $9, _x(%rip)
_x 是一个全局变量。基本上我不确定在这种情况下如何添加到全局变量,以及多处理器系统中这条线是否存在固有的竞争条件。
最佳答案
正如 duskwuff 所指出的,您需要一个 lock
前缀。
原因是:
addl $9,_x(%rip)
实际上是从内存系统的角度来看的三个“微操作”[这里的%eax
只是为了说明——从未真正使用过]:
mov _x(%rip),%eax
addl $9,%eax
mov %eax,_x(%rip)
这是一个有效的事件序列。这是由 lock
前缀保证的。最后,_x
将为 18:
# this is a valid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
addl $9,%eax
mov %eax,_x(%rip)
mov _x(%rip),%eax
addl $9,%eax
mov %eax,_x(%rip)
但是,如果没有锁
,我们可以得到:
# this is an invalid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
mov _x(%rip),%eax
addl $9,%eax addl $9,%eax
mov %eax,_x(%rip)
mov %eax,_x(%rip)
最后,_x
将是 9。序列的进一步困惑可能会产生 18。因此,根据两个 CPU 上微操作之间的确切顺序,我们可以得到 9 或 18。
我们可以让它变得更糟。如果 CPU 2 添加 8 而不是 9,则序列 without lock
可以产生以下任何一个:8、9 或 17
更新:
基于一些评论,只是为了澄清一下术语。
当我说微操作时……它是用引号引起来的,所以我是为了本文的讨论而创造了一个术语。它不意味着直接转换为 x86 处理器文献中定义的 x86 微指令。我本可以[也许应该]说步骤。
同样,虽然使用 x86 asm 表达步骤似乎最简单、最清晰,但我本来可以更抽象:
(1) FETCH_MEM_TO_MREG _x
(2) ADD_TO_MREG 9
(3) STORE_MREG_TO_MEM _x
不幸的是,这些步骤纯粹是在硬件逻辑中执行的(即程序无法看到它们或使用调试器逐步执行它们)。内存系统(例如高速缓存逻辑、DRAM Controller 等)会注意到(并且必须响应)步骤 (1) 和 (3)。 CPU 的 ALU 将执行步骤 (2),这对内存逻辑是不可见的。
请注意,一些 RISC CPU arches 没有在内存上工作的 add 指令,也没有锁前缀。见下文。
除了阅读一些文献之外,检查效果的一种实用方法是创建一个使用多线程(通过 pthreads
)并使用一些 C 原子操作和/或 pthread_mutex_lock 的 C 程序
。
此外,此页 Atomically increment two integers with CAS有一个我给出的答案,还有一个指向另一个人在 cppcon 上给出的视频谈话的链接(关于“无锁”实现)
在这个更通用的模型中,它还可以说明在没有正确记录锁定的数据库中会发生什么。
如何实现 lock
的实际机制可能是特定于 x86 模型的。
并且,可能,目标指令特定(例如,如果目标指令是 [say] addl
vs xchg
,则 lock
的工作方式不同)因为处理器可能能够使用更高效/特殊类型的内存周期(例如类似于原子“读-修改-写”的东西)。
在其他情况下(例如,数据对于单个周期来说太宽或跨越缓存行边界),它可能必须锁定整个内存总线(例如,获取全局锁并强制完全序列化),进行多次读取、进行更改、进行多次写入,然后然后解锁内存总线。这种模式类似于将某些东西包装在互斥锁锁定/解锁配对中的方式,仅在内存总线逻辑级别的硬件中完成
关于 ARM [一个 RISC cpu] 的注释。 ARM 只支持ldr r1,memory_address
、str r1,memory_address
,不 add r1,memory_address
。它只允许 add r1,r2,r3
[即它是“三元”] 或者可能是 add r1,r2,#immed
。为实现锁定,ARM 有两个特殊指令:ldrex
和strex
必须 配对。在上面的抽象模型中,它看起来像:
ldrex r1,_x
add r1,r1,#9
strex r1,_x
// must be tested for success and loop back if failed ...
关于c - 下面的程序集是原子的,如果不是,为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35332057/