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

C 为什么读取被其他线程修改的变量既不能是旧值也不能是新值,c,multithreading,thread-safety,data-race,C,Multithreading,Thread Safety,Data Race,例如,这里有几个人提到过它 如果两个线程在没有原子和锁的情况下操作同一个变量,那么读取该变量既不能返回旧值,也不能返回新值 我不明白为什么会发生这样的事情,我找不到这样的事情发生的例子,我认为负载和存储总是一个不会中断的指令,那么为什么会发生这种情况? < P>从语言的律师观点来看,即在C或C++规范中所说的,在不考虑程序可能运行的任何特定硬件的情况下,操作可以是已定义的,也可以是未定义的,如果操作未定义,则允许程序执行任何它想执行的操作,因为他们不想通过强制编译器编写器支持任何特定的操作行为来

例如,这里有几个人提到过它 如果两个线程在没有原子和锁的情况下操作同一个变量,那么读取该变量既不能返回旧值,也不能返回新值


<>我不明白为什么会发生这样的事情,我找不到这样的事情发生的例子,我认为负载和存储总是一个不会中断的指令,那么为什么会发生这种情况?

< P>从语言的律师观点来看,即在C或C++规范中所说的,在不考虑程序可能运行的任何特定硬件的情况下,操作可以是已定义的,也可以是未定义的,如果操作未定义,则允许程序执行任何它想执行的操作,因为他们不想通过强制编译器编写器支持任何特定的操作行为来限制语言的性能,而程序员无论如何都不应该允许这些操作发生


从实际的角度来看,在普通硬件上,最有可能读取既不旧也不新的值的场景是:;从广义上讲,另一个线程在线程从它读取的瞬间,而不是从另一部分读取变量的一部分,因此,从语言的律师角度来看,即从C或C++规范所说的,你得到了一半的旧值和一半的新值。在不考虑程序可能运行的任何特定硬件的情况下,操作可以是已定义的,也可以是未定义的,如果操作未定义,则允许程序执行任何它想执行的操作,因为他们不想通过强制编译器编写器支持任何特定的操作行为来限制语言的性能,而程序员无论如何都不应该允许这些操作发生

从实际的角度来看,在普通硬件上,最有可能读取既不旧也不新的值的场景是:;广义地说,在线程读取变量的那一刻,另一个线程已经写入了变量的一部分,但没有写入到另一部分,因此得到了旧值的一半和新值的一半

它已经被几个人提到过,例如这里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();
if (some_function()) j = 0;
    else j = 1;
由于修改j的唯一代码读取另一个线程正在编写的变量,编译器可以自由地假设代码永远不会执行,因此j始终是12,因此可以优化j的测试和对重要函数的调用。哎哟

下面是另一个例子:

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();
if (some_function()) j = 0;
    else j = 1;
如果实现认为某个_函数几乎总是返回true,并且可以证明某个_函数无法访问j,那么它完全可以将其优化为:

j = 0;
if (!some_function()) j++;
如果其他线程在没有锁的情况下使用j,或者j不是定义为原子的类型,这将导致代码严重中断

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

它已经被几个人提到过,例如这里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();
if (some_function()) j = 0;
    else j = 1;
由于修改j的唯一代码读取另一个线程正在编写的变量,编译器可以自由地假设代码永远不会执行,因此j始终是12,因此可以优化j的测试和对重要函数的调用。哎哟

下面是另一个例子:

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();
if (some_function()) j = 0;
    else j = 1;
如果实现认为某个_函数几乎总是返回true,并且可以证明某个_函数无法访问j,那么它完全可以将其优化为:

j = 0;
if (!some_function()) j++;
如果其他线程在没有锁的情况下使用j,或者j不是定义为原子的类型,这将导致代码严重中断


不要认为某些编译器优化虽然合法,但永远不会发生。随着编译器变得越来越智能,这一点一次又一次地折磨着人们。

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

如果您修改的值不是原子值或由互斥锁选通的值,则无法保证您读取的内容。我认为加载和存储总是一条指令,这是错误的。这是两次手术。此外,如果您写入某个可以缓存的内容,那么另一个CPU内核可能会使用过时的值,直到刷新或刷新缓存为止。@tadman:我认为这并不意味着只有一条指令加载和存储。他们的意思是加载是一条指令,存储是一条指令。这可能是错误的,这也是非原子加载和存储不能得到保证的原因,但这不是你的评论所说的。缓存中的过时值也不相关;原子访问也可能涉及过时的值。过时是一个排序问题,而不是原子性问题。如果您正在修改非原子或由互斥锁选通的值,则无法保证您读取的内容。我认为加载和存储总是一条指令,这是错误的。这是两次手术。此外,如果您写入某个可以缓存的内容,那么另一个CPU内核可能会使用过时的值,直到刷新或刷新缓存为止。@tadman:我认为这并不意味着只有一条指令加载和存储。他们的意思是加载是一条指令,存储是一条指令。这可能是错误的,这也是非原子加载和存储不能得到保证的原因,但这不是你的评论所说的。缓存中的过时值也不相关;原子访问也可能涉及过时的值。过时是一个排序问题,而不是原子性问题;如果没有理由的话,委员会就不会这样定义语言:能够在多个线程中读写而没有锁是有价值的,因此委员会不会拒绝用户这个价值,除非有理由这样做。问这个理由是什么是合理的问题。这个答案根本不能回答这个问题。这个答案和Eric的答案都是合理和有用的,谢谢@埃里克:我同意。我回答了实际提出的问题。用这样的答案回答这个问题是非常危险的,因为它表明,如果你想不出莱顿啤酒,或者认为你能想到的理由不适用,那么你就有一个你没有的保证。阅读你答案的人可能会想,我永远不会使用16位平台,所以我是安全的。@EricPostchil另外,如果平台必须让它工作,那么有大量的实际优化是不可能的。光是这一点就足以让它成为未定义的行为。例如,即使在未优化的代码无法写入内存的情况下,也要写入内存。Eric、Jeremy和David的答案都非常好,没有一个正确/最佳的答案,谢谢大家!C标准d
oes不存在于真空中;如果没有理由的话,委员会就不会这样定义语言:能够在多个线程中读写而没有锁是有价值的,因此委员会不会拒绝用户这个价值,除非有理由这样做。问这个理由是什么是合理的问题。这个答案根本不能回答这个问题。这个答案和Eric的答案都是合理和有用的,谢谢@埃里克:我同意。我回答了实际提出的问题。用这样的答案回答这个问题是非常危险的,因为它表明,如果你想不出莱顿啤酒,或者认为你能想到的理由不适用,那么你就有一个你没有的保证。阅读你答案的人可能会想,我永远不会使用16位平台,所以我是安全的。@EricPostchil另外,如果平台必须让它工作,那么有大量的实际优化是不可能的。光是这一点就足以让它成为未定义的行为。例如,即使在未优化的代码无法写入内存的情况下,也要写入内存。Eric、Jeremy和David的答案都非常好,没有一个正确/最佳的答案,谢谢大家!谢谢你,埃里克!这是很清楚的。请问这是唯一的条件吗?假设我的软件只在现代x86-64体系结构上工作,并且我的使用可以容忍过时的值,不使用原子和锁定是否安全?@1a1a11a:不,这不是唯一的条件。首先,请注意,目标硬件具有原子加载/存储指令这一事实并不意味着C实现使用它们。您不能假设“硬件具有此功能,因此我的C实现也具有此功能。”可能存在一些奇怪的情况,编译器认为它可以优化对对象x的某些访问,而它正在对对象y执行其他操作,并且它使用了一些奇怪的加载序列。或者,编译器决定节省空间并跨单词边界分割x以避免填充,但它需要两个加载。@1a1a11a:这些不一定是可能的事情,但关键是正确的工程从规范中得出结论:如果指定了,您可以编写您知道将以某种方式运行的代码。如果未指定,则无法确定。不幸的是,许多事情没有适当的记录,人们不得不在没有高质量保证的情况下继续进行,但这就是生活。我明白了,这里的答案/讨论非常有用。我学到了很多。非常感谢。谢谢你,埃里克!这是很清楚的。请问这是唯一的条件吗?假设我的软件只在现代x86-64体系结构上工作,并且我的使用可以容忍过时的值,不使用原子和锁定是否安全?@1a1a11a:不,这不是唯一的条件。首先,请注意,目标硬件具有原子加载/存储指令这一事实并不意味着C实现使用它们。您不能假设“硬件具有此功能,因此我的C实现也具有此功能。”可能存在一些奇怪的情况,编译器认为它可以优化对对象x的某些访问,而它正在对对象y执行其他操作,并且它使用了一些奇怪的加载序列。或者,编译器决定节省空间并跨单词边界分割x以避免填充,但它需要两个加载。@1a1a11a:这些不一定是可能的事情,但关键是正确的工程从规范中得出结论:如果指定了,您可以编写您知道将以某种方式运行的代码。如果未指定,则无法确定。不幸的是,许多事情没有适当的记录,人们不得不在没有高质量保证的情况下继续进行,但这就是生活。我明白了,这里的答案/讨论非常有用。我学到了很多。非常感谢。