Совместное использование памяти между потоками С++

Я новичок в потоковом режиме на С++, и я пытаюсь получить четкое представление о том, как память делится/не делится между потоками. Я использую std::thread с С++ 11. Из того, что я прочитал по другим вопросам 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);
}

Простите меня, если я испортил кучу вещей, синтаксис лямбда очень для меня. Но, надеюсь, все, что я пытаюсь сделать, является последовательным. Будет ли это работать так, как я ожидаю? А если нет, то что я недопонимаю?

Ответы

Ответ 1

Память - это память. Объект в С++ занимает некоторое место в памяти; это местоположение может находиться в стеке или в куче, или оно может быть статически распределено. Не имеет значения, где находится объект: любой поток, который имеет ссылку или указатель на объект, может получить доступ к объекту. Если два потока имеют ссылку или указатель на объект, то оба потока могут получить к нему доступ.

В вашей программе вы создаете рабочий поток (путем создания std::thread), который выполняет предоставленное им лямбда-выражение. Поскольку вы фиксируете как stackObj, так и heapObj по ссылке (используя по умолчанию [&] захват по умолчанию), эта лямбда имеет ссылки на оба этих объекта.

Эти объекты расположены в стеке основного потока (обратите внимание, что heapObj - объект типа указателя, который находится в стеке основного потока и указывает на динамически выделенный объект, расположенный в куче). Никаких копий этих объектов не производится; скорее, ваше лямбда-выражение имеет ссылки на объекты. Он напрямую изменяет stackObj и косвенно косвенно изменяет объект, на который указывает heapObj.

После того, как основной поток присоединяется к рабочему потоку, оба heapObj->x и stackObj.x имеют значение 1.


Если вы использовали значение захвата значения по умолчанию ([=]), ваше выражение лямбда было бы скопировано как stackObj, так и heapObj. Выражение stackObj.x++ в выражении лямбда увеличило бы копию, а stackObj, которую вы объявляете в main(), остался бы неизменным.

Если вы фиксируете значение heapObj по значению, копируется только сам указатель, поэтому, пока используется копия указателя, он все равно указывает на тот же динамически выделенный объект. Выражение heapObj->x++ приведет к разыменованию этого указателя, создав Obj, созданный с помощью new Obj(), и увеличит его значение. Затем вы наблюдали в конце main(), что heapObj->x был увеличен.

(Обратите внимание, что для изменения объекта, захваченного значением, выражение lambda должно быть объявлено mutable.)

Ответ 2

Я согласен с Джеймсом Макнеллисом в том, что heapObj->x и stackObj.x будет 1.

Кроме того, этот код только работает, потому что вы join сразу же после нереста потока. Если вы запустили поток, а затем выполнили больше работы во время его запуска, исключение может развернуть стек, и вдруг новый поток stackObj окажется недействительным. Вот почему совместное использование стековой памяти между потоками - это плохая идея, даже если это технически возможно.