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

How can I pass an object's member variable (field) as a reference to a thread safely?

假设我从类方法启动一个新线程并将“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;
};

这是一个没有未定义行为的安全解决方案吗?好像它的工作。有没有更好的and/or更“标准化”的方式?

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

  1. 将对象的生命周期延长到线程的生命周期。最简单的方法是使用对象的动态分配。此外,为避免内存泄漏,请使用 std::shared_ptr.
  2. 等智能指针
  3. 将线程运行时限制为对象的运行时。在销毁对象之前,只需加入线程即可。
  4. 告诉线程在销毁对象之前放开它。我只粗略描述一下,因为它是最复杂的方法,但是如果您以某种方式告诉线程它不能再使用该对象,那么您就可以销毁该对象而不会产生不利的副作用。

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