【问题标题】:Moving objects containing a running std::thread member移动包含正在运行的 std::thread 成员的对象
【发布时间】:2017-08-29 10:55:40
【问题描述】:

如何在对象中运行线程时正确使用移动语义?

示例:

#include <iostream>
#include <thread>
#include <vector>

struct A {
    std::string v_;
    std::thread t_;

    void start() {
        t_ = std::thread(&A::threadProc, this);
    }

    void threadProc() {
        for(;;) {
            std::cout << "foo-" << v_ << '\n';
            std::this_thread::sleep_for(std::chrono::seconds(5));
        }
    }
};

int main() {
    A m;
    {
        A a;
        a.v_ = "bar";
        a.start();
        m = std::move(a);
    }
    std::cout << "v_ = " << m.v_ << '\n'; /* stdout is 'v_ = bar' as expected    */
                                          /* but v_ in thread proc was destroyed */
                                          /* stdout in thread proc is 'foo-'     */
    m.t_.join();
    return 0;
}

我想在移动后使用类成员,但是当我离开作用域时,类成员被破坏并且 std::thread 按预期移动到新对象中,但它开始使用被破坏的成员。

在我看来是因为在线程初始化中使用了this 指针。

在这种情况下,最佳做法是什么?

【问题讨论】:

  • 为了完整起见,还应定义构造函数、析构函数和复制构造函数。而在这种情况下应该显式删除复制构造函数。
  • 嗯,不知道你能做些什么。你能重新审视一下这个要求吗?
  • 它不使用默认的ctors吗?无论如何,我有更大的样本,在 ctors 和 dtor 中具有调试输出,并且行为者是相同的。
  • @FrankSchmid:不;不要定义无意义的方法。
  • 线程正在执行A的成员函数。由于A 的前一个实例在移动后被破坏,线程使用的成员指针变得无效。您可以使用第二个独立对象实例,例如B(即 A 的成员)包含要执行的函数并将其移动到新的 A 实例。

标签: c++


【解决方案1】:

正如所写,它不会起作用。移动后,线程m.t_ 指向一个仍在运行的线程a.threadProc()。这将尝试打印a.v_

sn-p 甚至有两个问题:不仅a.v_ 被移出(因此它的值未指定),而且它也即将在另一个线程中被销毁,并且该销毁不是在其之后排序的使用。

由于对象需要保持足够长的生命周期,并且由于线程而具有不平凡的生命周期,因此您需要将其从堆栈中取出并从向量中取出。相反,使用std::shared_ptr 来管理生命周期。您可能需要将 shared_ptr 传递给线程,以避免在线程开始运行之前对象可能过期的竞争条件。你不能依赖std:shared_from_this

【讨论】:

  • 谢谢,shared_ptr 应该有所帮助
【解决方案2】:

在这种情况下,最佳做法是什么?

最好的做法是删除移动构造函数和移动赋值运算符以防止这种情况发生。您的对象要求 this 永远不会更改,并且您会遇到未定义的行为,因为在这种情况下,该对象是从您的线程下方抽出并随后被销毁的。

如果出于某种原因阻止移动违背了您的设计要求,那么有一些常见的方法对于任何有幸阅读和维护您的代码的人来说都是最有意义的。

  1. 使用 pimpl idiom 动态创建一个内部对象,该对象可以与外部对象一起移动。外部对象是可移动的,但内部对象不是。线程绑定到该对象,并且线程需要访问的任何内容也在该对象中。在您的情况下,您基本上会按原样采用结构并将其包装起来。基本思想是这样的:

    class MovableA
    {
    public:
        MovableA() : a_(std::make_unique<A>()) {}
        void start() { a_->start(); }
        A & a() const { return *a_; }
    private:
        std::unique_ptr<A> a_;
    };
    

    这种方法的好处是您可以移动MoveableA,而无需与正在运行的线程同步。

  2. 放弃使用堆栈分配的概念,只动态分配A。这与选项 1 具有相同的好处,并且更简单,因为您不必将类包装在任何东西中或提供访问器。

    std::unique_ptr<A> m;
    {
        auto a = std::make_unique<A>();
        a->v_ = "bar";
        a->start();
        m = std::move(a);
    }
    std::cout << "v_ = " << m->v_ << '\n';
    m->t_.join();
    

我开始编写一个选项 3,它避免了动态分配,而是将 this 的“浮动”版本绑定到 std::reference_wrapper,但我觉得我没有多想就搞错了,而且看起来很 hacky还是很可怕的。

底线是如果你想将对象保留在你的线程之外在线程中使用它,最好的做法是使用动态分配。

【讨论】:

  • Pimpl 在我的情况下是开销,无论如何感谢提示删除移动 ctor 以防依赖 this 指针。
【解决方案3】:

(替代答案,使用 C++17)

使用 lambda,您可以捕获 A 的副本。由于线程拥有 lambda,而 lambda 拥有副本,因此您不会遇到生命周期问题:

t_ = std::thread([*this](){threadProc();});

【讨论】:

  • 这在C++17之前无效吗?
  • @BoundaryImposition: AFAICT,是的,但 [tmp=*this] 可能是 C++14(我必须检查一下)。
  • 好吧,我猜我的意思是,请解释一下这在 C++17 之前是如何无效的。
  • @BoundaryImposition:很简单,它是无效的,因为 C++14 没有产生这种捕获列表的语法规则,也没有赋予它意义的语义规则。
  • 所以,更简单地说,捕获*this 的能力是C++17 中的新功能?
猜你喜欢
  • 2015-12-19
  • 2019-09-13
  • 2013-07-14
  • 2022-09-21
  • 1970-01-01
  • 1970-01-01
  • 2015-06-18
  • 1970-01-01
  • 2021-11-28
相关资源
最近更新 更多