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();
}
这个值可以打印两次相同的值吗?感觉标准应该在某处提供此保证。但不确定。
答案 0 :(得分:1)
C ++标准甚至不需要使用堆栈来实现函数调用(或者在这种意义上,线程具有堆栈)。
当前的C ++草案对此overlapping objects进行了说明:
如果一个对象嵌套在另一个对象中,或者如果至少一个对象是零大小的子对象并且它们是不同类型,则两个寿命不同且不是位域的对象可能具有相同的地址;否则,它们具有不同的地址并占用不相交的存储字节。
并在(非规范性)脚注中:
根据“假设”规则,如果程序无法观察到差异,则允许实现将两个对象存储在同一机器地址或根本不存储对象([intro.execution)。
在您的示例中,我认为线程并没有按照预期的那样正确同步,因此integer
对象的生存期不一定重叠,因此可以将两个对象放在相同的地址。
如果固定了代码以使其正确同步,并且将foo
手动内联到bar
中,以使integer
对象在其地址打印时仍然存在,则将必须可以将两个对象分配到不同的地址,因为差异 是可见的。
但是,没有一个告诉您在没有编译器帮助的情况下是否可以在C ++中实现堆栈协程。现实世界中的编译器对执行环境进行了假设,这些假设未反映在C ++标准中,而仅被ABI标准隐含。与堆栈切换协程特别相关的是以下事实:执行函数时线程描述符和线程局部变量的地址不会更改(因为它们的计算成本可能很高,并且编译器会发出代码以将其缓存在寄存器或寄存器中)。堆栈)。
这可能会发生:
协程在线程A上运行并访问errno
。
协程从线程A挂起。
协程在线程B上恢复。
协程再次访问errno
。
这时,线程B将访问线程A的errno
值,这很可能在此点上做了完全不同的事情。
如果协程仅在被挂起的同一线程上恢复,则可以避免此问题,这是非常严格的,可能不是大多数协程库作者所想到的。最糟糕的部分是,在大多数情况下,恢复在错误的线程上似乎很有用,因为某些不是局部于线程的,广泛使用的局部线程变量(例如errno
)不会立即产生在明显有问题的程序中。
答案 1 :(得分:0)
对于所有标准护理,当new __StackFrameFoo
需要堆栈框架时,实现将调用foo()
。那些人知道谁结束了。
主要规则是不同的对象具有不同的地址,并且包括“存在于堆栈中”的对象。但是该规则仅适用于同时存在的两个对象,然后仅适用于通过适当的线程同步进行比较的情况。当然,比较地址确实会阻碍优化器的工作,优化器可能需要为可能会被优化的对象分配地址。