代码之家  ›  专栏  ›  技术社区  ›  Curious

C++线程堆栈地址范围

  •  0
  • Curious  · 技术社区  · 5 年前

    C++标准是否提供了关于线程堆栈的非重叠性质的保证(如由 std::thread )?特别是,是否保证线程在进程的地址空间中为线程堆栈拥有自己的、独占的、分配的范围?这在标准中是哪里描述的?

    例如

    std::uintptr_t foo() {
        auto integer = int{0};
        return std::bit_cast<std::uintptr_t>(&integer); 
        ... 
    }
    
    void bar(std::uint64_t id, std::atomic<std::uint64_t>& atomic) {
        while (atomic.load() != id) {}
        cout << foo() << endl;
        atomic.fetch_add(1);
    }
    
    int main() {
        auto atomic = std::atomic<std::uint64_t>{0};
        auto one = std::thread{[&]() { bar(0, atomic); }};
        auto two = std::thread{[&]() { bar(1, atomic); }};
    
        one.join();
        two.join();
    }
    

    这个能打印两次相同的值吗?感觉标准应该在某个地方提供这种保证。但不确定…

    1 回复  |  直到 5 年前
        1
  •  1
  •   Florian Weimer    5 年前

    C++标准甚至不要求使用堆栈实现函数调用(或者在这个意义上线程具有堆栈)。

    当前C++草稿对此表示 overlapping objects :

    如果一个对象嵌套在另一个对象中,或者如果至少一个对象是大小为零的子对象,并且它们属于不同的类型,则两个具有重叠生存期的对象可能具有相同的地址;否则,它们具有不同的地址并占用不相交的存储字节。

    在(非规范性)脚注中:

    在“仿佛”规则下,允许实现在同一机器地址存储两个对象,或者如果程序无法观察到差异,则根本不存储对象([ intro.execution )

    在您的示例中,我认为线程没有像预期的那样正确同步,因此 integer 对象不一定重叠,因此两个对象可以放在同一个地址。

    如果代码被修复为正确同步,并且 foo 被手动内联到 bar ,以使 整数 对象在打印其地址时仍然存在,因此必须在不同的地址分配两个对象,因为 可观察到的

    然而,这并没有告诉您在没有编译器帮助的情况下是否可以在C++中实现。真实世界编译器对执行环境的假设没有反映在C++标准中,并且只被ABI标准所暗示。与堆栈交换协同路由特别相关的是,线程描述符和线程局部变量的地址在执行函数时不会更改(因为它们的计算成本很高,编译器会发出代码将它们缓存在寄存器或堆栈中)。

    这就是可能发生的情况:

    1. 协同程序在线程A上运行并访问 errno .

    2. 协程从线程A挂起。

    3. 在线程B上恢复协同程序。

    4. 协同访问 埃尔诺 再一次。

    此时,线程B将访问 埃尔诺 线程A的值,在这一点上它很可能正在做完全不同的事情。

    如果只在挂起协同程序的同一线程上恢复协同程序,则可以避免此问题,这是非常限制性的,而且可能不是大多数协同程序库作者所考虑的。最糟糕的是,在大多数情况下,恢复错误的线程似乎是可行的,因为一些广泛使用的线程局部变量(例如 埃尔诺 )不完全是线程本地的不会立即导致明显的程序错误。

        2
  •  0
  •   MSalters    5 年前

    对于所有的标准关注,实现调用 new __StackFrameFoo 什么时候 foo() 需要一个堆栈帧。谁知道结局在哪里。

    主要规则是不同的对象有不同的地址,其中包括“存在于堆栈上”的对象。但该规则仅适用于同时存在的两个对象,并且仅在通过适当的线程同步进行比较的情况下才适用。当然,比较地址确实会妨碍优化器,优化器可能需要为一个本来可以优化的对象分配地址。