14

今天我遇到了这个问题:

你有一个代码

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

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

我知道实际上它可以是任何东西。但我的内心告诉我,这counter++很可能会被翻译成单个汇编指令,如果两个线程都在同一个内核上执行,则为counter20。

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

4

11 回答 11

20

专门针对 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前缀。

于 2009-07-07T10:06:10.540 回答
8

并非总是如此——在某些架构上,一条汇编指令被翻译成一条机器代码指令,而在其他架构上则不然。

此外- 您永远不能假设您正在使用的程序语言正在将看似简单的代码行编译成一条汇编指令。此外,在某些架构上,您不能假设一个机器代码会自动执行。

相反,请使用适当的同步技术,具体取决于您编码的语言。

于 2009-07-07T08:54:55.520 回答
8

答案是:视情况而定!

这里有一些混乱,什么是汇编指令。通常,一条汇编指令被翻译成一条机器指令。例外是当您使用宏时——但您应该意识到这一点。

也就是说,问题归结为一个机器指令是原子的?

在过去的美好时光中,它是。但是今天,有了复杂的 CPU、长时间运行的指令、超线程……它不是。一些 CPU 保证某些递增/递减指令是原子的。原因是,它们非常适合非常简单的同步。

此外,一些 CPU 命令也不是那么成问题。当你有一个简单的提取(处理器可以在一块中提取的一条数据)时——提取本身当然是原子的,因为根本没有什么可以分割的。但是当你有未对齐的数据时,它又变得复杂了。

答案是:视情况而定。仔细阅读供应商的机器使用说明书。毫无疑问,它不是!

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

于 2009-07-07T09:12:03.737 回答
6
  1. 在没有超线程技术的单个 32 位处理器上对 32 位或更少整数变量的递增/递减操作是原子的。
  2. 在具有超线程技术的处理器或多处理器系统上,不保证自动执行递增/递减操作。
于 2009-07-07T08:55:15.503 回答
4

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

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

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

于 2009-07-07T09:08:18.237 回答
3

不,你不能假设这一点。除非它在编译器规范中明确说明。而且,没有人能保证一条汇编指令确实是原子的。在实践中,每条汇编指令都被翻译成微码操作的数量 - 微指令。
此外,竞争条件的问题与内存模型(一致性、顺序性、释放一致性等)紧密相关,对于每一个问题,答案和结果都可能不同。

于 2009-07-07T08:54:20.823 回答
3

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

于 2009-07-07T08:58:19.350 回答
2

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

push [address]

在 C 中,它类似于:

*stack-- = *address;

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

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

于 2013-05-01T05:17:29.997 回答
1

可能不是您问题的实际答案,但是(假设这是 C# 或另一种 .NET 语言)如果您想counter++真正成为多线程原子,您可以使用System.Threading.Interlocked.Increment(counter).

counter++有关为什么/如何不能是原子的许多不同方式的实际信息,请参阅其他答案。;-)

于 2009-07-07T09:47:03.480 回答
-1

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

为此,存在内存屏障(http://en.wikipedia.org/wiki/Memory_barrier

所以简而言之,虽然在 intel 上原子指令就足够了(带有相关的锁前缀),但在非 intel 上必须做更多的事情,因为内存 I/O 的顺序可能不同。

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

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

于 2009-07-09T10:38:22.193 回答
-3

我认为您会在访问时获得竞争条件。

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

于 2009-07-07T08:53:55.270 回答