上下文切换是否保留(保持变量的状态)变量的值,当它恢复任务时?

huangapple go评论76阅读模式
英文:

Does the context switch retain (keep state of variables) the value of the variables when it resumes the task?

问题

假设没有任何同步的伪代码:

count = 0; // 全局变量

线程1:

count = count + 1;

假设在读取 count 后,“捕获”了0,并发生了上下文切换。它转到另一个线程,发生了以下情况:

线程2:

count = count + 1; // Count = 1。在没有上下文切换的情况下完成所有操作

决定返回到线程1:

在完成线程1之后,count 的值会是1,因为当它被切换时它读取了0,而不是2。也就是说,

上下文切换会在恢复任务时保留变量的值吗?

英文:

Pseudocode (Assuming that there is no synchronization of any kind):

count = 0; // Global

Thread 1:

count = count + 1;

suppose that after reading count "captured" the 0 and context switch occurred. It went to another thread and the following happened:

Thread 2:

count = count + 1; // Count = 1. Finished all without context switching

Decides to return to thread 1:

after finishing thread 1, will count be worth 1 because it read 0 when it swapped, or will it be worth 2? That is,

does the context switch retain the value of the variables when it resumes the task?

答案1

得分: 2

为了真正回答你的问题... 以下内容确实可能发生:

线程1:将“count”加载到CPU寄存器“A”中。结果:A = 0
线程1:增加CPU寄存器A。结果:A = 1

(上下文切换)

线程2:加载“count” -> 0
线程2:增量 -> 1
线程2:将1存储在“count”中

(上下文切换)

线程1:从寄存器A中存储1到“count”中

之后,count仍然为1,并且一个增量已经“丢失”。在实际应用中,几乎任何事情都可能发生,包括计数器突然倒退或读取完全不合理的值。

跨上下文切换保留的是CPU的寄存器。为了对变量进行操作,CPU首先必须将变量从内存加载到寄存器中。然后,它可以对寄存器中的变量的副本进行操作。当完成操作时,它将从寄存器中的值写回内存(可能在增量之后立即,也可能在很久之后 - 这是未指定的)。如果另一个线程在此期间更改了变量,CPU寄存器中的值将已过时,您的程序将出现问题。

有所谓的原子变量(即std::atomic<int>),允许不会被上下文切换中断的“读取-修改-写入”操作(并且还可以同时在多个不同的CPU核上执行)。

在某些CPU架构上(没有一致性缓存的架构),由一个线程写入内存的数据甚至在执行所谓的释放操作之前不会被其他线程看到,这会使这些数据对其他线程可用。这意味着如果没有适当的同步,不同的线程甚至在所有写访问已经完成时仍然可以从变量中读取完全不同的值。一般来说,“释放”操作使数据可供执行“获取”操作的线程。这些操作可以是争夺和释放互斥锁,但也可以是具有适当语义的原子变量的访问(这些操作转换为特殊的机器指令,不仅访问变量还控制缓存一致性)。

请注意,获取/释放语义也是编译器的屏障:在没有屏障(和原子操作)的情况下,编译器可以自由地重新排序内存访问,只要程序执行“就像”未经修改一样,不考虑线程。这意味着编译器可以根据自己的意愿重新排序、省略和复制内存读取和写入。释放屏障(即互斥锁释放)防止编译器在屏障之后将访问“下移”,从而使屏障使先前的写入对其他线程可见。获取屏障(即互斥锁获取)防止编译器在屏障之前将访问“上移”,使其他线程的写入对执行屏障的线程可见。当一个线程上的释放与另一个线程上的获取匹配时(即它们都使用相同的互斥锁或相同的原子变量),释放线程写入的数据变得对获取线程可见,您可以安全地在线程之间传输数据而不会发生问题。

有关所有这些原子操作/无锁操作的更多信息,我建议你观看Herb Sutter在CppCon 2014的演示:

第1部分:https://www.youtube.com/watch?v=c1gO9aB9nbs

第2部分:https://www.youtube.com/watch?v=CmxkPChOcvw

英文:

To actually answer your question... The following can (and will!) indeed happen:

Thread 1: Load &quot;count&quot; into CPU register &quot;A&quot;. Result: A = 0
Thread 1: Increment CPU register A. Result: A = 1

(Context switch)

Thread 2: Load &quot;count&quot; -&gt; 0
Thread 2: Increment -&gt; 1
Thread 2: Store 1 in &quot;count&quot;

(Context switch)

Thread 1: Store 1 from register A in &quot;count&quot;

Afterwards, count is still 1, and one increment has been "lost". In practice, pretty much anything can happen, including the counter suddenly going backwards or reading totally nonsensical values.

What's preserved across a context switch is the CPU's registers. In order to operate on a variable, the CPU first has to load the variable from memory into a register. Then it can operate on the copy of the variable in its register. When it's done, it'll write the value from the register back into memory (potentially immediately after the increment, or potentially much later - it's unspecified). If another thread changes the variable in the meanwhile, the value in the CPU's register will be outdated and your program breaks.

There are so-called atomic variables (i.e. std::atomic&lt;int&gt;) that allow "read-modify-write" operations that can't be interrupted by a context switch (and can also be done concurrently on multiple different CPU cores).

On some CPU architectures (ones without coherent caches), data written to memory by one thread isn't even visible to other threads until a so-called release operation has been executed that makes this data available to other threads. This means that, if there isn't proper synchronization, different threads could read totally different values from a variable even when all write accesses to it have already completed. In general, "release" operations make data available to threads that perform an "acquire" operation. These operations could be the acquiring and releasing of a mutex, but it could also be an access to an atomic variable with suitable semantics (these translate to special machine instructions that not only access the variable but also control cache coherence).

Note that acquire/release semantics are also barriers for the compiler: In the absence of barriers (and atomics), the compiler is free to re-order memory accesses as long as the program executes "as-if" it was unmodified, not taking threads into account. This means that a compiler can re-order, omit, and duplicate memory reads and writes as it pleases. A release barrier (i.e. mutex release) prevents the compiler from moving accesses "down" after the barrier, so that the barrier makes previous writes available to other threads. An acquire barrier (i.e. mutex acquire) prevents the compiler from moving accesses "up" before the barrier, so that the barrier makes other threads' writes visible to the thread that executed the barrier. When a release on one thread matches up with an acquire on another (i.e. they both use the same mutex or the same atomic variable), data written by the releasing thread becomes visible to the acquiring thread, and you can transfer data between threads safely without everything blowing up.

For more info on all of this atomics / lock-free stuff, I'd recommend you to watch this presentation by Herb Sutter at CppCon 2014:

Part 1: https://www.youtube.com/watch?v=c1gO9aB9nbs

Part 2: https://www.youtube.com/watch?v=CmxkPChOcvw

答案2

得分: 0

你可以简单地查看 count = count + 1 编译成的汇编代码(假设是 x86-64 架构):

mov     eax, DWORD PTR count[rip]
add     eax, 1
mov     DWORD PTR count[rip], eax

很明显,不管第一次上下文切换是发生在第一行还是第二行之后,count 的最终值都将是1。

英文:

You can simply inspect the assembly code that count = count + 1 compiles to (assuming x86-64 architecture):

mov     eax, DWORD PTR count[rip]
add     eax, 1
mov     DWORD PTR count[rip], eax

It is clear to see that the final value of count will be 1 no matter whether the first context switch happens after the first or the second line.

huangapple
  • 本文由 发表于 2023年2月9日 03:10:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75390627.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定