一个汇编指令总是以原子方式执行吗?

时间:2009-07-07 08:50:35

标签: multithreading assembly atomic race-condition

今天我遇到了这个问题:

你有一个代码

static int counter = 0;
void worker() {
    for (int i = 1; i <= 10; i++)
        counter++;
}

如果从两个不同的线程调用worker,那么在counter完成后counter++会有什么值?

我知道实际上它可能是任何东西。但我的内部胆量告诉我,counter很可能被翻译成单个汇编指令,如果两个线程都在同一个核心上执行,{{1}}将为20。

但是,如果这些线程在不同的内核或处理器上运行,那么它们的微码中是否存在竞争条件呢?是否可以将一个汇编指令视为原子操作?

11 个答案:

答案 0 :(得分:18)

特别针对x86,关于您的示例:counter++,可以通过多种方式进行编译。最简单的例子是:

inc counter

这转化为以下微操作:

  • counter加载到CPU上的隐藏寄存器
  • 递增寄存器
  • 将更新的注册表存储在counter

这基本上与:

相同
mov eax, counter
inc eax
mov counter, eax

请注意,如果某个其他代理在加载和商店之间更新counter,则在商店之后它不会反映在counter中。此代理可以是同一内核中的另一个线程,同一CPU中的另一个内核,同一系统中的另一个CPU,甚至是使用DMA(直接内存访问)的一些外部代理。

如果您想保证此inc是原子的,请使用lock前缀:

lock inc counter

lock保证任何人都无法在加载和商店之间更新counter


关于更复杂的指令,您通常不能假设它们将以原子方式执行,除非它们支持lock前缀。

答案 1 :(得分:7)

并非总是 - 在某些体系结构上,一个汇编指令被转换为一个机器代码指令,而在其他架构上则没有。

此外 - 您可以从不假设您正在使用的程序语言正在将一段看似简单的代码编译成一条汇编指令。而且,在某些体系结构中,您不能假设一个机器代码将以原子方式执行。

使用适当的同步技术,取决于您编码的语言。

答案 2 :(得分:7)

答案是:这取决于!

这里有一些混乱,汇编指令是什么。通常,一个汇编程序指令被转换为恰好一个机器指令。例外情况是你使用宏 - 但你应该知道这一点。

那说,问题归结为一个机器指令原子?

在过去的好时光中,它是。但今天,复杂的CPU,长时间运行的指令,超线程,......事实并非如此。某些CPU保证某些递增/递减指令是原子的。原因是,它们非常简单,可以进行非常简单的同步。

一些CPU命令也不是那么成问题。当你有一个简单的提取(处理器可以一件获取的一个数据) - 提取本身当然是原子的,因为根本没有任何东西可以分割。但是当你有未对齐的数据时,它会再次变得复杂。

答案是:这取决于。仔细阅读供应商的机器使用说明书。有疑问,它不是!

编辑: 哦,我现在看到了,你也要求++计数器。 “最有可能被翻译”的陈述根本不可信。这在很大程度上还取决于编译器当然!当编译器进行不同的优化时,它会变得更加困难。

答案 3 :(得分:5)

  1. 在没有超线程技术的单个32位处理器上对32位或更少整数变量的递增/递减操作是原子的。
  2. 在具有超线程技术的处理器上或在多处理器系统上,增量/减量操作不能保证在原子上执行。

答案 4 :(得分:4)

Nathan的评论无效: 如果我正确记住我的Intel x86汇编程序,INC指令仅适用于寄存器,不能直接用于内存位置。

因此,计数器++不会是汇编程序中的单个指令(只是忽略后增量部分)。它至少有三条指令:加载计数器变量到寄存器,递增寄存器,加载寄存器回到计数器。这仅适用于x86架构。

简而言之,除非语言规范指定并且您使用的编译器支持规范,否则不要依赖它是原子的。

答案 5 :(得分:3)

不,你不能假设这一点。除非在编译器规范中明确说明。而且没有人能保证单个汇编指令确实是原子的。实际上,每个汇编程序指令都被转换为微码操作的数量 - 微操作。
竞争条件的问题与记忆模型(连贯性,顺序性,释放连贯性等)紧密相关,对于每一个,答案和结果可能不同。

答案 6 :(得分:2)

另一个问题是,如果你没有将变量声明为volatile,那么生成的代码可能不会在每次循环迭代时更新内存,只有在循环结束时内存才会更新。

答案 7 :(得分:1)

如果你希望counter++真的是多线程原子,你可以使用System.Threading.Interlocked.Increment(counter),这可能不是你问题的实际答案,但是(假设这是C#或其他.NET语言)

请参阅其他答案,了解有关counter++无法原子的多种不同方式的实际信息。 ; - )

答案 8 :(得分:1)

在大多数情况下,。实际上,在x86上,您可以执行指令

push [address]

,在C中,它将是:

*stack-- = *address;

这在一条指令中执行两次内存传输

这基本上不可能在1个时钟周期内完成,尤其是因为一个内存传输在一个周期内也是不可能的!

答案 9 :(得分:-1)

在许多其他处理器上,内存系统和处理器之间的分离更大。 (通常这些处理器可能是小端或大端,取决于内存系统,如ARM和PowerPC),如果内存系统可以重新排序读取和写入,这也会对原子行为产生影响。

为此目的,存在记忆障碍(http://en.wikipedia.org/wiki/Memory_barrier

简而言之,虽然原子指令对于intel(具有相关的锁前缀)已经足够了,但是必须在非英特尔上执行更多操作,因为内存I / O可能不是相同的顺序。

将英特尔的“无锁”解决方案移植到其他架构时,这是一个众所周知的问题。

(请注意,x86上的多处理器(非多核)系统似乎也需要内存屏障,至少在64位模式下。

答案 10 :(得分:-3)

我认为您在访问时会遇到竞争条件。

如果你想确保递增计数器的原子操作,那么你需要使用++ counter。