如果同一个线程可以在不同的 CPU 上调度,为什么它不会产生问题?

If same thread can get scheduled on different CPUs, why does it not create problems?

当线程在 multi-processor/multi-core 台机器上执行时,它们会导致 CPU 缓存从 RAM 加载数据。 如果线程应该是 'see' 相同的数据,则不能保证,因为 thread1 可能会在其 CPU 的(即当前正在执行的位置)缓存中导致更新,并且此更改不会立即可见到线程 2。

为了解决这个问题,Java 等编程语言提供了 volatile.

等结构

我很清楚在不同 CPUs 上执行多个线程的问题是什么。

我很确定给定的线程在其生命周期内不会绑定到特定的 CPU,并且可以在不同的 CPU 上安排到 运行。但我不清楚为什么这不会导致类似于在不同 CPU 上使用不同线程的问题? 毕竟这个线程可能导致 CPU 的缓存更新尚未写入 RAM。如果这个线程现在被安排在另一个 CPU 上,它不会访问陈旧数据吗?

到目前为止,我能想到的唯一可能性是,线程的上下文切换涉及将线程可见的所有数据写回 RAM,并且当线程被调度到 CPU 时,它的缓存从 RAM 中刷新(以防止线程看到陈旧的值)。但是,从性能的角度来看,这看起来有问题,因为时间切片意味着线程一直在被调度。

能否请专家指点一下真实情况?

在单个线程上,发生的操作之间存在 happens-before 关系,无论调度如何完成。这是由作为 Java memory model contract promised in the Java Language Specification:

的一部分的 JVM 实现强制执行的

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

操作系统具体如何实现这一点取决于实现。

对于那些可能没有仔细阅读对不同答案的所有评论的人,这里是我在脑海中建模的内容的简化摘要(如果有任何不正确的地方,请随时发表评论。我将对此进行编辑post)

  1. http://tutorials.jenkov.com/java-concurrency/volatile.html 不准确,会出现这样的问题。 CPU 缓存总是一致的。如果 CPU 1 已写入其缓存中的内存地址 X 并且 CPU 2 从其自己的缓存中读取相同的内存地址(在 CPU 1 写入其缓存之后),然后 CPU 2 将读取 CPU 1 所写的内容。不需要特殊说明来执行此操作。
  2. 然而,现代 CPUs 也有存储缓冲区。它们用于在缓冲区中累积写入以提高性能(以便这些写入可以在自己的时间提交到缓存,从而使CPU免于等待缓存一致性协议完成)。
  3. CPU 的存储缓冲区中的内容对其他 CPU 尚不可见。
  4. 此外,CPUs 和编译器为了提高性能可以自由地重新排序指令,只要它不改变计算结果(从单线程的角度来看)
  5. 此外,某些编译器优化可能会将变量完全移动到 CPU 例程的寄存器,从而 'hiding' 它们来自共享内存,从而使对该变量的写入对其他线程不可见。
  6. 上面第 3、4、5 点是 Java 公开 Volatile 等关键字的原因。当您使用 volatile 时,如果指令会破坏 'happens-before' 保证,JVM 本身不会重新排序指令。 JVM 还要求 CPU 不要使用内存 barrier/fence 指令重新排序。 JVM 也不使用任何阻止 'happens-before' 保证的优化。总的来说,如果对易失性内存的写入已经发生,则此后由另一个线程进行的任何读取都将确保正确的值不仅可用于该字段,而且可用于在写入易失性字段时第一个线程可见的所有字段。

以上与关于单线程在其生命周期中使用不同 CPU 的问题有什么关系?

  1. 如果在CPU上执行的线程已经写入了它的缓存,这里就不用再考虑了。即使线程稍后使用另一个 CPU,由于缓存一致性,它也能够看到自己的写入。
  2. 如果线程的写入在存储缓冲区中等待并且它被移出 CPU,上下文切换确保线程从存储缓冲区的写入被提交到缓存。之后就和第1点一样了。
  3. 任何仅在 CPU 寄存器中的状态,无论如何都会作为上下文切换的一部分进行备份和恢复。

由于以上几点,单个线程在其生命周期内执行不同的 CPUs 时不会遇到任何问题。

现代 CPU 上的缓存总是一致的。因此,如果一个存储由一个 CPU 执行,那么在另一个 CPU 上的后续加载将看到该存储。换句话说:缓存是真相的来源,内存只是一个溢出桶,可能与现实完全不同步。因此,由于缓存是连贯的,因此 CPU 线程 运行.

并不重要

同样在单个 CPU 系统上,由于编译器优化,缺少 volatile 可能会导致问题。例如,编译器可以将一个变量从循环中提升出来,然后由 1 个线程进行写入,无论它是否在同一个 CPU 上 运行ning,都不会被另一个线程看到。 =10=]

我建议不要考虑硬件。如果您使用 Java,请确保您了解 Java 内存模型 (JMM)。这是一个抽象模型,它阻止了从硬件的角度思考,因为 JMM 需要 运行 独立于硬件。

it is not clear to me why that does not cause problems similar to the one with different threads on different CPUs? After all this thread may have caused an update in one CPU's cache which is yet to be written to RAM. If this thread now gets scheduled on another CPU wouldn't it have access to stale data?

是的,它可能可以访问陈旧数据,但它更有可能在其缓存中拥有无用的数据——与它所需的内存无关。首先,来自 OS 的权限(如果写得正确)不会让一个程序看到另一个程序的数据——是的,最近新闻中有很多关于硬件漏洞的故事,所以我正在谈论它是如何发生的应该 工作。如果另一个进程被交换到 CPU.

中,缓存将被清除

缓存内存是否过时是架构缓存一致性系统的时序或是否跨越内存栅栏的函数。

context switching of threads involves writing all the data visible to the thread back to RAM and that when a thread gets scheduled on a CPU, its cache gets refreshed from RAM (to prevent thread seeing stale values).

虽然它的高速缓存在计划时没有刷新,但这与发生的情况非常接近。当线程上下文切换出 CPU 时,所有脏内存页都会刷新到 RAM。当一个线程被交换到 CPU 时,缓存要么被刷新(如果来自另一个进程)要么包含可能对传入线程没有帮助的内存。这会导致初始内存访问的页面错误率更高,这意味着线程需要花费更长的时间来访问内存,直到它需要的行被加载到缓存中。

However this looks problematic from performance point of view as time-slicing means threads are getting scheduled all the time.

是的,性能受到了影响。这突出了为什么正确调整线程池的大小如此重要。如果您有太多线程 运行 CPU 密集型任务,您可能会因为上下文切换而导致性能下降。如果线程正在等待 IO,那么增加线程数是必须的,但如果你只是在计算一些东西,使用更少的 CPUs 可以导致更高的吞吐量,因为每个 CPU 可以在处理器中停留更长时间缓存命中率与未命中率上升。