Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/assembly/5.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Assembly 为什么我是++;单核机器上的线程不安全?_Assembly - Fatal编程技术网

Assembly 为什么我是++;单核机器上的线程不安全?

Assembly 为什么我是++;单核机器上的线程不安全?,assembly,Assembly,可能重复: 您能在汇编代码级别为我描述一下,为什么在单核机器上,从两个不同线程递增一个值被认为是不安全的 i++有三种操作: 将i提取到寄存器中 递增寄存器 将其写回i 在这些操作之间,调度程序可能会中断线程,以便不同的线程可以运行(并修改i)。threadone读取旧值 计时器中断停止 内核恢复线程2 线程2读取旧值 穿两条线 线程2编写它 计时器响了 内核恢复线程1 线程1递增 线程一存储 现在你落后了一步。你的问题被标记为assembler,但询问的是i++。您无法保证C代码中的i+

可能重复:


您能在汇编代码级别为我描述一下,为什么在单核机器上,从两个不同线程递增一个值被认为是不安全的

i++
有三种操作:

  • i
    提取到寄存器中
  • 递增寄存器
  • 将其写回
    i

在这些操作之间,调度程序可能会中断线程,以便不同的线程可以运行(并修改
i
)。

threadone读取旧值

计时器中断停止

内核恢复线程2

线程2读取旧值

穿两条线

线程2编写它

计时器响了

内核恢复线程1

线程1递增

线程一存储


现在你落后了一步。

你的问题被标记为assembler,但询问的是i++。您无法保证C代码中的
i++
将编译为一条改变内存的指令。如果有多个线程用一条指令从内存加载
i
,用另一条指令递增,用第三条指令将其写回内存,第一个和第三个线程之间的线程切换可能会导致对
i
的某些更新丢失。

在读取值和写入值之间,是什么阻止系统取消一个线程的调度?当然,这种情况不太可能发生,但在标准操作系统上,内核可以随时得到中断,并决定另一个线程应该运行。此时,两个线程将读取相同的值,并且两个线程的增量相同。但是,第二个线程可以运行另一个时间段,增加数千倍,然后当第一个线程重新调度时,第二个线程将通过写入过时的值来终止所有向前的进程。

考虑为类似
i++
的语句生成的指令。当然,这将取决于您的体系结构/指令集,但它可能与以下内容有关:

LOAD    @i, r0    ;load the value of 'i' into a register from memory
ADD     r0, 1     ;increment the value in the register
STORE   r0, @i    ;write the updated value back to memory

现在考虑在操作系统中如何实现多线程,而不管机器有多少内核。在最基本的层面上,操作系统需要一些工具来中断当前线程的执行,保存其状态,并执行到不同线程的上下文切换。操作系统无法自动知道用户线程中的哪些指令应被视为原子操作,并且能够在任意两条指令之间启动上下文切换

那么,如果操作系统在
LOAD
ADD
之间执行从一个线程到另一个线程的上下文切换,会发生什么情况呢?假设
i
开始时的值为0,因此当交换第一个线程时,
r0
将设置为0。操作系统将此值保存为该线程状态的一部分。现在第二个线程运行,并执行相同的
LOAD
语句。内存中的值仍然是0,因此
r0
再次将0加载到内存中。线程增加该值并将其写回内存,将
i
的值设置为1。现在,第一个线程恢复执行,操作系统将
r0
的值恢复为0,作为上下文切换的一部分。第一个线程现在执行其增量,将
r0
设置为1,并且值1再次存储在
i
中。现在,
i
的值不正确,因为应用了两个增量,但该值仅增加了1


简而言之,尽管在高级语言中,
i++
是一条语句,但它会生成多条汇编语言指令,操作系统/运行时环境不会将这些指令视为原子指令,除非您在其周围添加额外的同步逻辑。

如果处理器没有一条指令可以增加内存位置的内容,编译器将不得不执行以下操作:

 load      location, registerA
 increment registerA
 store     registerA, location
因此,即使任何一条指令是原子的,序列也不是原子的。即使有一个

increment location

指令没有保证编译器会使用它。例如,编译器可能已经进行了一些优化,并且正在使用一个寄存器来保存一些常用值,只是在编译器语言内存模型中的任何排序规则要求的时间将其存储回内存。

无法预测从单个核上的两个线程执行的指令序列。当两个线程都尝试执行i++时,以下是一个可能的序列,但效果相当于执行一次i++:

load i        # thread 1
system interrupt
load i        # thread 2, now i++ in thread 1 is not complete
increment i   # thread 2
store i       # thread 2
system interrupt
increment i   # thread 1, actually the same value
store i

好的,那么,为了确保我理解,如果某个线程在寄存器中有一个内存地址的内容,并且该线程被切换出并返回,我们将不会用该内存地址的当前内容更新寄存器中的值,而是使用切换前该寄存器中的任何值?@Dejas:当然。寄存器未链接到内存位置。谢谢。我认为操作系统可能足够聪明,可以跟踪R0内容的来源,因此在第一个线程恢复时使R0无效。@Dejas:smart够聪明吗?这样做会破坏所有的代码,并使上下文切换更加昂贵。谢谢@aroth。我有点困惑,当操作系统恢复第一个线程时,它是否会使R0的内容无效并从内存中重新读取。现在我很清楚,无论其他线程做了什么,R0都将是R0以前的值。在x86上,这实际上可以在一条指令中完成,例如“inc[eax]”,其中eax是指向包含“i”的内存的寄存器。这假设“i”甚至存储在内存中(不只是针对寄存器进行优化),并且