【问题标题】:Does one assembler instruction always execute atomically? [duplicate]一条汇编指令是否总是以原子方式执行? [复制]
【发布时间】:2010-11-08 15:10:13
【问题描述】:

今天我遇到了这个问题:

你有一个代码

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

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

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

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

【问题讨论】:

    标签: multithreading assembly atomic race-condition


    【解决方案1】:

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

    push [address]
    

    在 C 语言中类似于:

    *stack-- = *address;
    

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

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

    【讨论】:

      【解决方案2】:

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

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

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

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

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

      【讨论】:

      • 关于你的编辑,你为什么把自己限制在64位模式?为什么您将自己限制在多处理器系统上?事实上,您不仅不能在多核系统上假设任何排序,而且对于单核、多线程系统也不能。
      • Afaik 在多核(但不是多处理器)系统上的常见缓存和缓存一致性会导致同步内存系统。 IIRC 64 位是因为当 64 位内存负载越过缓存线边界时,需要双倍的内存周期才能获取。但请阅读 Intel 或 AMD ABI/处理器手册(搜索 sfence),并亲自查看。我只是想在不更新研究的情况下重复我记得的内容。
      【解决方案3】:

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

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

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

      【讨论】:

      • 一条汇编指令可以翻译成多条微码指令
      • 我知道我不能假设任何事情,我在问题中写道:)
      • @Yuval,因此我投了反对票:“是的 - 在大多数架构上,一条汇编指令被翻译成一条机器代码指令。”对于纯 RISC 架构可能如此,但对于 CISC 甚至像 x86 这样的混合架构绝对不是这样。
      • 很公平 - 由于当今最常见的处理器是 RISC,我编辑了我的答案以反映这一点
      【解决方案4】:

      因 Nathan 的评论无效: 如果我没记错我的 Intel x86 汇编器,INC 指令只适用于寄存器,不能直接适用于内存位置。

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

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

            【解决方案7】:

            答案是:视情况而定!

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

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

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

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

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

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

            【讨论】:

            • 我赞成“某些 CPU 保证某些递增/递减指令是原子的”,我强调 some。另外,请注意,即使是简单的 fetch 也不一定是原子的。例如,如果它跨越一个缓存线边界,它可以分两部分完成。如果数据类型大于缓存线,即使是对齐的数据。
            • 嗨内森,感谢您的评论!我没有考虑缓存线。但你是对的,这也可能是一个额外的并发症。我会说,一个高速缓存行(大部分)是正常字长(32/64 位)的倍数,不是吗?这就是我添加“处理器可以一体获取”的原因)。
            【解决方案8】:

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

            【讨论】:

              【解决方案9】:

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

              【讨论】:

                【解决方案10】:
                1. 在没有超线程技术的单个 32 位处理器上,对 32 位或更少整数变量的递增/递减操作是原子操作。
                2. 在采用超线程技术的处理器或多处理器系统上,不能保证自动执行递增/递减操作。

                【讨论】:

                • 这一点我知道,我想知道会发生什么?微码会进入竞争状态?
                • 是的,在第 2 种情况下。)你有一个竞争条件,结果不能保证总是 20。
                • 这是错误的。即使在单线程单 CPU 系统上,您仍然可以让外部代理通过 DMA 或 PCI 更新内存。即使是简单的 inc memory 指令也不应该被认为是原子的。您仍然必须使用 lock inc memory
                • 同意@Nathan,在这两种情况下都需要锁定。
                • Nathan Fellman 是正确的,除非您使用内存锁,否则不能保证共享内存上的增量是原子的,即使在单核 CPU 上,您的线程也可以在读取和增量之间被抢占或其他像 DMA 这样的机制可以与您的更新竞争。
                【解决方案11】:

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

                如果您想确保在递增计数器时进行原子操作,那么您需要使用 ++counter。

                【讨论】:

                • 现代编译器并不重要,都是一样的操作。
                猜你喜欢
                • 1970-01-01
                • 2021-12-14
                • 2017-03-19
                • 1970-01-01
                • 2018-04-28
                • 1970-01-01
                • 2020-07-09
                • 2015-07-19
                • 1970-01-01
                相关资源
                最近更新 更多