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

Multithreading 一条汇编指令总是以原子方式执行吗?,multithreading,assembly,atomic,race-condition,Multithreading,Assembly,Atomic,Race Condition,今天我遇到了一个问题: 你有密码吗 static int counter = 0; void worker() { for (int i = 1; i <= 10; i++) counter++; } 静态整数计数器=0; void worker(){ 对于(inti=1;i我认为您将在访问时获得一个竞争条件 如果您想确保递增计数器中的原子操作,那么您需要使用++计数器。不,您不能假设这一点。除非在编译器规范中明确说明。而且,没有人能够保证一条汇编指令确实是原子的

今天我遇到了一个问题:

你有密码吗

static int counter = 0;
void worker() {
    for (int i = 1; i <= 10; i++)
        counter++;
}
静态整数计数器=0;
void worker(){

对于(inti=1;i我认为您将在访问时获得一个竞争条件


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

不,您不能假设这一点。除非在编译器规范中明确说明。而且,没有人能够保证一条汇编指令确实是原子的。实际上,每条汇编指令都被转换为micro的数量ode操作-uops。

此外,竞争条件的问题与内存模型(一致性、顺序性、释放一致性等)密切相关,对于每一个问题,答案和结果都可能不同。

并不总是-在某些体系结构上,一条汇编指令被转换为一条机器代码指令,而在其他体系结构上则不是

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

请使用适当的同步技术,具体取决于所用的语言

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

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

    Nathan的评论无效: 如果我没有记错我的英特尔x86汇编程序,INC指令只适用于寄存器,而不直接适用于内存位置

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


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

    答案是:这取决于

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

    也就是说,问题归根结底是一条机器指令是原子的吗

    在过去的好日子里是这样。但今天,复杂的CPU、长时间运行的指令、超线程……不是这样。一些CPU保证某些递增/递减指令是原子指令。原因是,它们非常整洁,可以进行非常简单的同步

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

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

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

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


    请参阅其他答案,以了解有关为什么/how
    计数器+++
    不能是原子的许多不同方式的实际信息。-

    特别针对x86,关于您的示例:
    计数器+++
    ,有多种编译方法。最简单的示例是:

    inc counter
    
    这转化为以下微操作:

    • 计数器
      加载到CPU上的隐藏寄存器
    • 递增寄存器
    • 将更新后的寄存器存储在计数器中
    这基本上与:

    mov eax, counter
    inc eax
    mov counter, eax
    
    请注意,如果其他一些代理在加载和存储之间更新了
    计数器
    ,它将不会反映在存储之后的
    计数器
    中。此代理可能是同一内核中的另一个线程、同一CPU中的另一个内核、同一系统中的另一个CPU,甚至是使用DMA(直接内存访问)的某个外部代理

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

    lock inc counter
    
    lock
    保证加载和存储之间没有人可以更新
    计数器



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

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

    为此,存在内存障碍()

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

    将“无锁”解决方案从Intel移植到其他体系结构时,这是一个已知的问题

    (请注意,多处理器(非多核)系统
    push [address]
    
    *stack-- = *address;