为什么读取一个被其他线程修改的变量既不能是旧值也不能是新值

why reading a variable modified by other threads can be neither old value nor new value

有好几个人提到过,例如这里 c++ what happens when in one thread write and in second read the same object? (is it safe?) 如果两个线程在没有原子和锁定的情况下对同一个变量进行操作,则读取该变量既不能return旧值也不能读取新值。

我不明白为什么会发生这种情况,我找不到发生这种情况的例子,我认为加载和存储总是一条不会被中断的指令,那为什么会发生这种情况?

从语言律师的角度来看(即根据 C 或 C++ 规范的说法,不考虑程序可能 运行 在任何特定硬件上),操作是已定义或未定义的,如果操作是未定义的,那么程序实际上可以做它想做的任何事情,因为他们不想通过强迫编译器编写者支持程序员永远不允许的操作的任何特定行为来限制语言的性能无论如何都会发生。

从实际的角度来看,最有可能的情况(在普通硬件上)是 "word-tearing" 情况;您读取的值既不是旧的也不是新的。其中(广义上讲)另一个线程在您的线程从变量读取的那一刻写入了变量的一部分,但没有写入另一部分,因此您得到旧值的一半和新值的一半。

It has been mentioned by several, for example here c++ what happens when in one thread write and in second read the same object? (is it safe?) that if two threads are operating on the same variable without atomics and lock, reading the variable can return neither the old value nor the new value.

正确。未定义的行为是未定义的。

I don't understand why this can happen and I cannot find an example such things happen, I think load and store is always one single instruction which will not be interrupted, then why can this happen?

因为未定义的行为是未定义的。不要求您能够想到任何可能出错的方法。永远不要认为因为你想不出某种方法可以破坏某些东西,就意味着它不能破坏。

例如,假设有一个函数在其中进行了非同步读取。编译器可以得出结论,因此永远无法调用此函数。如果它是唯一可以修改变量的函数,那么编译器可以忽略对该变量的读取。例如:

int j = 12;

// This is the only code that modifies j
int q = some_variable_another_thread_is_writing;
j = 0;

// other code
if (j != 12) important_function();

由于唯一修改 j 的代码读取另一个线程正在写入的变量,编译器可以自由假设代码 永远不会 执行,因此 j 始终 为 12,因此可以优化 j 的测试和对 important_function 的调用。哎哟

这是另一个例子:

if (some_function()) j = 0;
    else j = 1;

如果实现认为some_function几乎总是returntrue并且可以证明some_function不能访问j,那么它是完全合法的将其优化为:

j = 0;
if (!some_function()) j++;

如果其他线程在没有锁的情况下乱用 jj 不是定义为原子的类型,这将导致您的代码严重中断。

不要永远认为某些编译器优化虽然合法,但永远不会发生。随着编译器变得越来越聪明,这一次又一次地烧毁了人们。

例如,C 可以在仅支持 16 位内存访问的硬件上实现。在这种情况下,加载或存储 32 位整数需要两条加载或存储指令。执行这两条指令的线程可能会在它们的执行之间被中断,并且另一个线程可能会在第一个线程恢复之前执行。如果另一个线程加载,它可能会加载一个新部分和一个旧部分。如果它存储,它可能存储两个部分,并且第一个线程在恢复时将看到一个旧部分和一个新部分。其他此类混合也是可能的。