下面的程序集是原子的,如果不是,为什么?
Is the following Assembly Atomic, If not, Why?
addl, , _x(%rip)
_x 是一个全局变量。基本上我不确定在这种情况下如何添加到全局变量以及多处理器系统中这条线是否存在固有的竞争条件。
没有。在处理器读取 _x
的旧值和写回新值之间有一个微小的 window;如果另一个 CPU 在那一刻写入 _x
,该值将被覆盖。
在指令中添加 LOCK
前缀将使操作成为原子操作。
正如 duskwuff 所指出的,您需要一个 lock
前缀。
原因是:
addl ,_x(%rip)
从内存系统的角度来看实际上是三个"micro operations"[这里%eax
只是为了说明--从未真正使用过]:
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
这是一个有效的事件序列。这是由 lock
前缀保证的。最后,_x
将变成18:
# this is a valid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
但是,如果没有 lock
,我们可以得到:
# this is an invalid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
mov _x(%rip),%eax
addl ,%eax addl ,%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 控制器等)会注意到(并且必须响应)步骤 (1) 和 (3)。 CPU 的 ALU 将执行步骤 (2),这对内存逻辑是不可见的。
请注意,某些 RISC CPU 架构没有在内存上工作的添加指令,也没有锁定前缀。见下文。
除了阅读一些文献之外,检查效果的一种实用方法是创建一个使用多线程(通过 pthreads
)并使用一些 C 原子操作的 C 程序 and/or pthread_mutex_lock
.
此外,此页面 Atomically increment two integers with CAS 有我给出的答案以及 link 另一个人在 cppcon 上的视频谈话(关于 "lockless" 实施)
在这个更通用的模型中,它还可以说明在没有正确记录锁定的数据库中会发生什么。
如何实现 lock
的实际机制可能是特定于 x86 模型的。
并且,可能,目标指令特定(例如,如果目标指令是 [say] addl
vs xchg
,lock
的工作方式不同),因为处理器可能能够使用更多 efficient/special 类型的内存周期(例如类似原子的 "read-modify-write")。
在其他情况下(例如数据对于单个周期来说太宽或跨越缓存行边界),它可能必须锁定整个内存总线(例如获取全局锁并强制完全序列化),做多次读取,进行更改,进行多次写入,然后 然后 解锁内存总线。这种模式类似于将某些东西包装在互斥体 lock/unlock 配对中的方式,仅在内存总线逻辑级别的硬件中完成
关于 ARM [a RISC cpu] 的注释。 ARM只支持ldr r1,memory_address
、str r1,memory_address
,不支持add r1,memory_address
。它只允许 add r1,r2,r3
[即它是 "ternary"] 或者可能是 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 ...
addl, , _x(%rip)
_x 是一个全局变量。基本上我不确定在这种情况下如何添加到全局变量以及多处理器系统中这条线是否存在固有的竞争条件。
没有。在处理器读取 _x
的旧值和写回新值之间有一个微小的 window;如果另一个 CPU 在那一刻写入 _x
,该值将被覆盖。
在指令中添加 LOCK
前缀将使操作成为原子操作。
正如 duskwuff 所指出的,您需要一个 lock
前缀。
原因是:
addl ,_x(%rip)
从内存系统的角度来看实际上是三个"micro operations"[这里%eax
只是为了说明--从未真正使用过]:
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
这是一个有效的事件序列。这是由 lock
前缀保证的。最后,_x
将变成18:
# this is a valid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
mov _x(%rip),%eax
addl ,%eax
mov %eax,_x(%rip)
但是,如果没有 lock
,我们可以得到:
# this is an invalid sequence
# cpu 1 # cpu 2
mov _x(%rip),%eax
mov _x(%rip),%eax
addl ,%eax addl ,%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 控制器等)会注意到(并且必须响应)步骤 (1) 和 (3)。 CPU 的 ALU 将执行步骤 (2),这对内存逻辑是不可见的。
请注意,某些 RISC CPU 架构没有在内存上工作的添加指令,也没有锁定前缀。见下文。
除了阅读一些文献之外,检查效果的一种实用方法是创建一个使用多线程(通过 pthreads
)并使用一些 C 原子操作的 C 程序 and/or pthread_mutex_lock
.
此外,此页面 Atomically increment two integers with CAS 有我给出的答案以及 link 另一个人在 cppcon 上的视频谈话(关于 "lockless" 实施)
在这个更通用的模型中,它还可以说明在没有正确记录锁定的数据库中会发生什么。
如何实现 lock
的实际机制可能是特定于 x86 模型的。
并且,可能,目标指令特定(例如,如果目标指令是 [say] addl
vs xchg
,lock
的工作方式不同),因为处理器可能能够使用更多 efficient/special 类型的内存周期(例如类似原子的 "read-modify-write")。
在其他情况下(例如数据对于单个周期来说太宽或跨越缓存行边界),它可能必须锁定整个内存总线(例如获取全局锁并强制完全序列化),做多次读取,进行更改,进行多次写入,然后 然后 解锁内存总线。这种模式类似于将某些东西包装在互斥体 lock/unlock 配对中的方式,仅在内存总线逻辑级别的硬件中完成
关于 ARM [a RISC cpu] 的注释。 ARM只支持ldr r1,memory_address
、str r1,memory_address
,不支持add r1,memory_address
。它只允许 add r1,r2,r3
[即它是 "ternary"] 或者可能是 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 ...