C++线程之间的内存共享

23

我刚接触C++中的多线程,并且试图清楚地了解线程之间内存如何共享/不共享。我正在使用C++11的std::thread

根据我在其他SO问题上阅读到的信息,堆栈内存只属于一个线程,而内存是在线程之间共享的。因此,从我对堆栈和堆的理解来看,以下应该是正确的:

#include <thread>
using namespace std;

class Obj {
public:
    int x;
    Obj(){x = 0;}
};

int main() {
    Obj stackObj;
    Obj *heapObj = new Obj();
    thread t([&]{
        stackObj.x++;
        heapObj->x++;
    });
    t.join();
    assert(heapObj->x == 1);
    assert(stackObj.x == 0);
}

如果我搞砸了很多东西,请原谅,lambda语法对我来说非常新。但是希望我尝试做的事情是连贯的。 这样会按照我的预期执行吗?如果不是,我有什么误解吗?


前提有点错误。堆栈内存不是线程本地的,它可以在线程之间共享。 - juanchopanza
请查看此链接以获取更详细的答案:http://candrews.net/blog/2011/07/understanding-c-0x-lambda-functions/ - 因为您使用了[&],所有变量都是按引用传递的,因此两个计数器都将为1。如果您使用了[=],则会成功断言。 - Dark Falcon
@juanchopanza 我的错,我知道线程本地有一个特定的含义(尽管我不完全确定是什么),所以我改变了我的问题。这就是“前提”的问题吗? - Michael Dorst
问题在于堆栈内存不属于一个线程,它可以被任意数量的线程访问。 - juanchopanza
2个回答

31

内存就是内存。在C++中,对象占据着内存中的某个位置;该位置可以位于堆栈或堆上,也可以是静态分配的。对象所在的位置并不重要:任何拥有对该对象引用或指针的线程都可以访问该对象。如果两个线程都有对该对象的引用或指针,则两个线程都可以访问它。

在您的程序中,通过构造一个std::thread来创建一个工作线程,并执行您提供的lambda表达式。因为您使用了[&]引用默认值捕获stackObjheapObj,所以该lambda表达式引用了这两个对象。

这些对象都位于主线程的堆栈上(请注意,heapObj是一个指向位于堆上的动态分配对象的指针类型对象,而该指针位于主线程的堆栈上)。没有复制这些对象;相反,您的lambda表达式引用这些对象。它直接修改stackObj并间接修改heapObj指向的对象。

在主线程与工作线程合并后,heapObj->xstackObj.x的值都为1


如果您使用了值捕获默认值([=]),则lambda表达式会复制 stackObjheapObj。lambda表达式中的stackObj.x++将增加副本,而您在main()中声明的stackObj将保持不变。

如果您按值捕获heapObj,那么只会复制指针本身,因此尽管使用了指针的副本,但它仍指向相同动态分配的对象。表达式heapObj->x++将对该指针进行解引用,得到通过new Obj()创建的Obj并增加其值。然后在main()结束时,您会观察到heapObj->x已经增加。(请注意,为了修改按值捕获的对象,lambda表达式必须声明为mutable。)

2
如果“内存就是内存”,那这一切又是什么意思呢?https://dev59.com/z3I-5IYBdhLWcg3wy76n - Michael Dorst
5
每个线程都有自己的堆栈,是的。只有拥有该堆栈的线程才能在堆栈上分配内存。因此,在您的程序中,主线程和工作线程都有自己独立的堆栈。但是,没有任何阻止线程与另一个线程共享其自己堆栈上分配的对象的指针或引用。这正是您在这里所做的:主线程在其堆栈上创建一个Obj对象,然后与工作线程共享对该对象的引用。两个线程都可以访问该对象。 - James McNellis
哦,好的,这样就更有意义了。 - Michael Dorst
6
那个问题的被接受答案声称“每个线程都有一个私有堆栈”。“私有”是正确的,但是它的意思是“只有拥有该堆栈的线程可以在其上创建和销毁对象”,而不是“其他任何线程都无法访问该内存”。 - James McNellis
1
顺便提一下,在堆栈上共享对象给其他线程是一个不好的做法。堆栈上的对象只在作用域关闭之前存在。可能会出现这样的情况,即对象已经被销毁,但您仍然试图从另一个线程中使用它。最好复制或移动堆引用,除非您绝对确定存储对象的线程在其他线程摆脱堆栈引用之前不会进一步进行。 - polkovnikov.ph

1

我同意James McNellis的观点,heapObj->xstackObj.x将会是1

此外,这段代码之所以仅仅能够工作,是因为你在启动线程后立即进行了join。如果你在线程运行时做更多的工作,异常可能会展开堆栈,导致新线程的stackObj无效。这就是为什么即使在技术上是可能的情况下,共享线程间的堆栈内存也是一个坏主意。


谢谢你的提醒,但是你说的话对我来说并没有太多意义。"unwinding the stack" 到底是什么意思,为什么会发生这种情况呢? - Michael Dorst
1
当C++程序抛出异常时,运行时会调用当前堆栈帧中每个对象的析构函数,然后弹出该帧并重复此过程,直到捕获异常。https://dev59.com/bnE95IYBdhLWcg3wY8_6 - japreiss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接