如何“未定义”竞争条件?

时间:2013-02-22 08:51:59

标签: c++ race-condition undefined-behavior

假设我定义了以下C ++对象:

class AClass
{
public:
    AClass() : foo(0) {}
    uint32_t getFoo() { return foo; }
    void changeFoo() { foo = 5; }
private:
    uint32_t foo;
} aObject;

该对象由两个线程T1和T2共享。 T1在循环中不断调用getFoo()以获得一个数字(如果之前未调用changeFoo(),则该数字将始终为0)。在某些时候,T2调用changeFoo()来改变它(没有任何线程同步)。

对于现代计算机架构和编译器,T1获得的值与现代计算机架构和编译器的差异是否有任何实用机会?到目前为止,我调查的所有汇编代码都使用32位内存读写,这似乎可以节省操作的完整性。

其他原始类型怎么样?

实用意味着您可以举一个现有架构或符合标准的编译器的示例,其中理论上可以实现此(或具有不同代码的类似情况) 。我将 modern 这个词留下了一点主观性。


编辑:我可以看到很多人注意到我不应该期待5次被阅读。这对我来说完全没问题,我没有说我这样做(虽然感谢你指出这方面的问题)。我的问题更多的是关于上述代码可能发生何种数据完整性违规。

7 个答案:

答案 0 :(得分:9)

实际上,据我所知,除了05之外你不会看到任何其他内容(可能是一些奇怪的16位架构,32位int,但事实并非如此)。

但是,您是否真的看到5并不能保证。

假设我是编译器。

我明白了:

while (aObject.getFoo() == 0) {
    printf("Sleeping");
    sleep(1);
}

我知道:

  • printf无法更改aObject
  • sleep无法更改aObject
  • getFoo不会更改aObject(感谢内联定义)

因此我可以安全地转换代码:

while (true) {
    printf("Sleeping");
    sleep(1);
}

因为在此循环中没有其他人访问aObject,根据C ++标准

这就是未定义的行为意味着:炸毁期望。

答案 1 :(得分:3)

实际上,所有主流的32位架构都以原子方式执行32位读写操作。你永远不会看到0或5以外的任何东西。

答案 2 :(得分:2)

在实践中(对于那些没有阅读过问题的人),任何潜在的问题都归结为unsigned int的存储操作是否是一个原子操作,在大多数情况下(如果不是全部)机器你可能会编写代码,它将是。

请注意,标准未对此进行说明;它特定于您要定位的体系结构。我无法想象一个调用线程会使05以外的任何内容变为红色的场景。

关于标题......我不知道不同程度的“未定义行为”。 UB是UB,它是二进制状态。

答案 3 :(得分:2)

不确定你在寻找什么。在大多数现代体系结构中,即使在getFoo()被调用之后,0始终返回changeFoo的可能性也非常明显。对于任何体面的编译器,几乎可以保证getFoo()始终返回相同的值,无论是否调用changeFoo,如果在紧密循环中调用它。

当然,在任何实际程序中,都会有其他读取和写入,与foo中的更改完全不同步。

最后, 16位处理器,某些编译器也可能会uint32_t未对齐,因此访问不会是原子的。 (当然,您只更改其中一个字节中的位,因此这可能不是问题。)

答案 4 :(得分:2)

  

现代计算机架构和编译器是否有可能T1获得的值与0或5不同?那么其他原始类型呢?

当然 - 无法保证以原子方式写入和读取整个数据。 在实践中,您最终可能会在部分写入期间发生读取。什么可能被打断,何时发生取决于几个变量。因此在实践中,结果可能很容易随着大小和类型的对齐而变化。当然,随着您的程序从平台移动到平台以及ABI发生变化,也可能会引入这种差异。此外,可观察的结果可能会随着优化的增加和其他类型/抽象的引入而变化。编译器可以自由地优化你的大部分程序;也许是完全的,取决于实例的范围(另一个在OP中未考虑的变量)。

超越优化器,编译器和硬件特定管道:内核甚至可以影响处理此内存区域的方式。您的程序是保证每个对象的内存所在的位置?可能不是。您的对象的内存可能存在于单独的虚拟内存页面上 - 您的程序采取了哪些步骤来确保以一致的方式为所有平台/内核读取和写入内存? (没有,显然)

简而言之:如果您不能按照抽象机器定义的规则进行游戏,则不应使用所述抽象机器的接口(例如,如果C ++抽象机器的规范真的不适合您,那么您应该理解并使用汇编需求 - 非常不可能)。

  

到目前为止我调查的所有汇编程序代码都使用32位内存读写,这似乎可以节省操作的完整性。

这是对“诚信”的非常浅薄的定义。你所拥有的只是(伪)顺序一致性。同样,编译器只需要在这种情况下表现得好 - 这远非严格的一致性。浅层期望意味着即使编译器实际上没有按照某种理想或意图进行优化并执行读写操作,结果将实际上无用 - 您的程序通常会观察到更改在它发生之后很久。

根据具体情况,保证

,主题仍然无关紧要

答案 5 :(得分:1)

未定义的行为意味着编译器可以按照自己的意愿执行操作。他基本上可以改变你的程序,做他喜欢的事情,例如点一份比萨饼。

请参阅@Matthieu M.回答一个比这个更少讽刺的版本。我不会删除这个,因为我认为这些评论对于讨论很重要。

答案 6 :(得分:0)

未定义的行为保证与未定义的单词一样未定义 从技术上讲,可观察行为毫无意义,因为它只是未定义的行为,编译器不需要向您显示任何特定行为。它可能会像您认为的那样工作,也可能会烧毁您的计算机,任何事情都可能发生。