首页 > 解决方案 > C++:一个对象可以同时存储和不存储吗?

问题描述

我一直在争论关于多线程环境中局部变量的极端案例。

问题是关于形成的程序,如:

std::mutex mut;

int main()
{
    std::size_t i = 0;
    doSomethingWhichMaySpawnAThreadAndUseTheMutex();
    mut.lock();
    i += 1;        // can this be reordered?
    mut.unlock();
    return i;
}

问题围绕着是否i += 1可以重新排序以发生在互斥锁之上。

显而易见的部分是mut.lock() 发生在之前 i += 1,所以如果任何其他线程可能能够观察到的值i,编译器就必须不增加它。从 C++ 规范的 3.9.2.3 开始,“如果 T 类型的对象位于地址 A,则表示值为地址 A 的 cv T* 类型的指针指向对象,而不管值如何获得。"" 这意味着如果我使用任何方法获取指向 的指针i,我可以期望看到正确的值。

但是,规范确实声明编译器可以使用“as-if”规则不给对象一个内存地址(第 1.8.6 节的脚注 4)。例如,i可以存储在没有内存地址的寄存器中。在这种情况下,将没有内存地址可以指向,因此编译器可以证明没有其他线程可以访问i.

我感兴趣的问题是,如果编译器不执行这种“as-if”优化,并且确实存储了对象,该怎么办。编译器是否允许存储i,但重新排序就像i实际上没有存储一样?从实现的角度来看,这意味着它i可能存储在堆栈上,因此可以有一个指针指向它,但是让编译器假设没有人可以看到i,然后重新排序?

标签: c++multithreadinglanguage-lawyerundefined-behavior

解决方案


只要在没有这些优化的情况下可以合法地获得(“as-if”)程序执行的可观察结果,编译器就可以执行优化。[1]所以这个问题以一种误导的方式使用“as-if”,如果不是真的问一个倒退的问题:

编译器是否允许存储i,但重新排序就像i实际上没有存储一样?

这询问是否允许编译器做任何事情,只要程序执行的结果可以通过优化获得。这不是要问的问题。该问题应使用未优化的行为作为参考。所以更像是:“编译器是否允许重新排序语句?” 答案是肯定的,只要可观察到的结果不变。这个特定函数的外部没有被告知如何访问i,因此应该允许编译器在它的周围使用之间的任何地方实现增量(特别是:它的定义和return语句)。

话虽如此,在这种情况下,我希望编译器做的既不是提供i内存地址,也不是将其视为寄存器变量。我希望编译器将其视为常量,有效地将您的函数更改为:

int main()
{
    doSomethingWhichMaySpawnAThreadAndUseTheMutex();
    mut.lock();
    mut.unlock();
    return 1;
}

只要您无法检测到它已经完成(没有直接检查机器代码),就允许这样做。

注意:
[1]使用“could have been”表示承认 C++ 规范的某些部分使用了“unspecified”一词。这些部分允许编译器做出选择(在处理非健壮代码时)可能会改变可观察的行为。也就是说,可以有一组允许的行为,而不是单个允许的行为。只要结果保留在该集合中,就允许进行优化。


推荐阅读