1

有几个人提到过,例如这里的c++ 在一个线程中写入并在第二个线程中读取同一个对象时会发生什么?(安全吗?) 如果两个线程在没有原子和锁的情况下对同一个变量进行操作,则读取该变量既不能返回旧值也不能返回新值。

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

4

3 回答 3

2

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

从实际的角度来看,最有可能的场景(在通用硬件上)您读取的值既非旧也非新的将是“撕词”场景;其中(广义而言)另一个线程在您的线程从中读取变量的那一刻写入了变量的一部分,但没有写入另一部分,因此您获得了旧值的一半和新值的一半。

于 2020-07-03T22:52:41.183 回答
2

有几个人提到过,例如这里的 c++ 在一个线程中写入并在第二个线程中读取同一个对象时会发生什么?(安全吗?)如果两个线程在没有原子和锁的情况下对同一个变量进行操作,则读取该变量既不能返回旧值也不能返回新值。

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

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

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

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

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几乎总是返回true并且可以证明some_function无法访问j,那么将其优化为:

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

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

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

于 2020-07-03T23:02:10.153 回答
2

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

于 2020-07-03T23:17:15.157 回答