5

假设我定义了以下 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(),它将始终为 0 )。changeFoo()在某些时候,T2 调用changeFoo()来更改它(没有任何线程同步)。

对于现代计算机体系结构和编译器,T1 获得的值是否有可能不同于0 或 5?到目前为止,我调查的所有汇编代码都是使用 32 位内存读写,这似乎保存了操作的完整性。

其他原始类型呢?

实用意味着您可以给出一个现有架构或符合标准的编译器的示例,其中(或具有不同代码的类似情况)在理论上是可能的。我让现代这个词有点主观。


编辑:我可以看到很多人注意到我不应该期望 5 被阅读。这对我来说很好,我没有说我这样做(尽管感谢您指出这方面)。我的问题更多的是关于上述代码可能会发生什么样的数据完整性违规。

4

7 回答 7

11

0在实践中,除了或5据我所知,您不会看到任何其他东西(可能是一些奇怪的 16 位架构和 32 位架构,int但情况并非如此)。

但是,不能保证您是否真正看到5

假设我是编译器。

我懂了:

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

我知道:

  • printf不能改变aObject
  • sleep不能改变aObject
  • getFoo不会改变aObject(感谢内联定义)

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

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

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

这就是未定义行为的含义:夸大预期。

于 2013-02-22T09:14:57.073 回答
4

在实践中,所有主流的 32 位架构都以原子方式执行 32 位读写。除了 0 或 5,你永远不会看到任何东西。

于 2013-02-22T08:57:40.103 回答
2

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

请注意,标准没有说明这一点;它特定于您所针对的架构。我无法想象调用线程会变成除0or以外的任何东西的场景5

至于标题......我不知道不同程度的“未定义行为”。UB就是UB,它是一个二元状态。

于 2013-02-22T08:56:15.217 回答
2

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

当然,在任何真正的程序中,都会有其他的读写操作,这对于foo.

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

于 2013-02-22T09:04:43.937 回答
2

对于现代计算机体系结构和编译器,T1 获得的值是否有可能不同于 0 或 5?其他原始类型呢?

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

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

简而言之:如果您不能遵守抽象机定义的规则,则不应使用该抽象机的接口(例如,如果C++抽象机的规范确实不足以满足您的需求,则应该理解并使用汇编--极不可能)。

到目前为止,我研究的所有汇编代码都是使用 32 位内存读写,这似乎保存了操作的完整性。

这是对“诚信”的一个非常肤浅的定义。您所拥有的只是(伪)顺序一致性。同样,编译器只需要表现得好像在这种情况下一样——这远非严格的一致性。肤浅的期望意味着即使编译器实际上没有进行破坏性优化并按照某种理想或意图执行读取和写入,结果实际上也是无用的——您的程序通常会在发生变化后“很长时间”观察到变化。

考虑到您可以保证的具体内容,该主题仍然无关紧要。

于 2013-02-22T09:38:42.207 回答
1

未定义的行为意味着编译器可以为所欲为。他基本上可以改变你的程序来做他喜欢的任何事情,例如点披萨。

看,@Matthieu M. 回答比这个不那么讽刺的版本。我不会删除它,因为我认为评论对讨论很重要。

于 2013-02-22T08:53:51.403 回答
0

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

于 2013-02-22T08:54:02.893 回答