如何安全地传递对象的成员变量(字段)作为对线程的引用?



假设我从一个类方法中启动一个新线程,并传递"this"作为新线程的lambda的参数。如果对象在线程使用"this"中的内容之前被销毁,那么它可能是未定义行为。举个简单的例子:

#include <thread>
#include <iostream>

class Foo
{
public:
Foo() : m_bar{123} {}
void test_1()
{
std::thread thd = std::thread{[this]()
{
std::cout << m_bar << std::endl;
}};
thd.detach();
}
void test_2()
{
test_2(m_bar);
}
void test_2(int & bar)
{
std::thread thd = std::thread{[this, & bar]()
{
std::cout << bar << std::endl;
}};
thd.detach();
}
private:
int m_bar;
};

int main()
{
// 1)
std::thread thd_outer = std::thread{[]()
{
Foo foo;
foo.test_1();
}};
thd_outer.detach();
// 2)
{
Foo foo;
foo.test_1();
}
std::cin.get();
}

结果

(对于原始项目,我必须使用VS19,因此异常消息最初来自该IDE。)

  1. 从thd_outer开始,test_1和test_2要么抛出异常(异常抛出:读访问违规),要么打印0(而不是123)。
  2. 没有thd_outer他们似乎正确。

我在Linux下用GCC试过同样的代码,他们总是打印123。

哪一个是正确的行为?我认为是UB,在这种情况下,都是"正确的"。如果它不是未定义的,那么为什么它们不同呢?

我希望123或垃圾总是因为对象仍然有效(123)或有效但销毁和a)内存尚未被重用(123)或重用(垃圾)。异常是合理的,但究竟是什么抛出它(仅VS)?

我想到了一个可能的解决方案:

class Foo2
{
public:
Foo2() : m_bar{123} {}
~Foo2()
{
for (std::thread & thd : threads)
{
try
{
thd.join();
}
catch (const std::system_error & e)
{
// handling
}
}
}
void test_1()
{
std::thread thd = std::thread{[this]()
{
std::cout << m_bar << std::endl;
}};
threads.push_back(std::move(thd));
}
private:
int m_bar;
std::vector<std::thread> threads;
};

这是一个安全的解决方案,没有未定义的行为吗?好像起作用了。有没有更好的和/或更"标准化"的?路吗?

忘掉成员变量或类吧。接下来的问题是,我如何确保线程不使用对已被销毁的对象的引用。有两种方法可以有效地确保线程在对象被销毁之前结束,再加上第三种更复杂的方法。

  1. 将对象的生命周期延长到线程的生命周期。最简单的方法是使用对象的动态分配。此外,为了避免内存泄漏,可以使用智能指针,如std::shared_ptr
  2. 将线程运行时限制为对象的运行时。在销毁对象之前,只需加入线程。
  3. 告诉线程在销毁对象之前释放它。因为这是最复杂的方法,所以我只会简单地描述一下,但是如果你以某种方式告诉线程它不能再使用这个对象,那么你就可以在没有副作用的情况下销毁这个对象。

这就是说,一个建议:您正在(至少)两个线程之间共享对象。访问它需要同步,这本身就是一个复杂的主题。