通过加入析构函数来等待异步工作是否安全?

Is it safe to wait for asynchronous work by joining in the destructor?

假设我有一个 class 可以 运行 一些异步代码,并且该异步代码使用那个 class 实例来执行调用成员函数、读取数据成员等操作. 显然 class 实例必须比后台线程长,以便这些访问是安全的。在析构函数中加入后台线程就足以保证这一点了吗?例如:

#include <iostream>
#include <thread>

class foo final
{
public:
    foo() = default;

    void bar() {
        std::cout << "Hopefully there's nothing wrong with using " << this << "\n";
    }

    void bar_async() {
        if (!m_thread.joinable()) {
            m_thread = std::thread{&foo::bar, this};
        }
    }

    ~foo() {
        if (m_thread.joinable()) {
            std::cout << "Waiting for " << m_thread.get_id() << "\n";
            m_thread.join();
        }
    }

private:
    std::thread m_thread;
};

int main() {
    foo f;
    f.bar_async();
}

具体来说,我担心 object lifetime rules:

For any object of class types whose destructor is not trivial, lifetime ends when the execution of the destructor begins.

... after the lifetime of an object has ended and before the storage which the object occupied is reused or released, the following uses of the glvalue expression that identifies that object are undefined: ...

  • Access to a non-static data member or a call to a non-static member function.

但对我来说,严格阅读上述内容也意味着直接从 ~foo() 内部调用 this->bar() 是未定义的,"obviously" 并非如此。

我的直觉是否定的。这是因为 thread::join 可以抛出异常,而您不希望异常从您的析构函数中逃脱。如果您将其包装在 try catch 中并正确处理异常,则可能没问题。

cppreference 是正确的,但它是在谈论从对象而不是从析构函数内部访问成员。如果我们查看 [[=​​22=]]/1,我们会看到

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. For an object with a non-trivial destructor, referring to any non-static member or base class of the object after the destructor finishes execution results in undefined behavior.

强调我的

因此,只要我们在析构函数中,我们仍然可以使用成员对象,因为在析构函数的作用域结束之前,它们不会被销毁。

所以,在这里调用线程上的 join 没问题。如果您考虑一下,如果不是,那么如果访问它们所指的互斥量是未定义的行为,那么诸如锁守卫之类的东西将毫无用处。