如何在对象中运行线程正确使用移动语义?
样品:
#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
指针。
在这种情况下,最佳做法是什么?
答案 0 :(得分:4)
正如所写,它不会起作用。移动后,线程m.t_
引用仍在运行a.threadProc()
的线程。那将尝试打印a.v_
。
该代码段甚至有两个问题:不仅a.v_
被移动(因此它的值未指定),但它也将在另一个线程中被销毁,并且该销毁未按顺序排序 - 使用
由于对象需要保持足够长的时间,并且由于线程而具有非平凡的生命周期,因此您需要将其从堆栈中移出并从向量中移出。相反,使用std::shared_ptr
来管理生命周期。您可能需要将shared_ptr
传递给线程,以避免在线程开始运行之前对象可能会到期的竞争条件。您不能依赖std:shared_from_this
。
答案 1 :(得分:3)
在这种情况下,最佳做法是什么?
最佳做法是删除移动构造函数并移动赋值运算符以防止这种情况发生。您的对象要求this
永远不会更改,并且您将获得未定义的行为,因为在这种情况下,对象从您的线程下方被甩出并随后被破坏。
如果由于某种原因阻止移动违背了您的设计要求,那么有一些常见的方法会让任何有幸阅读和维护代码的人都感到最有意义。
使用 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
而无需与正在运行的线程同步。
放弃使用堆栈分配的概念,并动态分配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
,但我觉得如果不考虑它就会弄错,而且无论如何它看起来很糟糕和可怕。
最重要的是,如果你想让对象保持在你的线程之外并且在线程中使用它,最好的做法是使用动态分配。
答案 2 :(得分:2)
(替代答案,使用C ++ 17)
使用lambda,您可以捕获A
的副本。由于线程拥有lambda而lambda拥有副本,因此没有生命周期问题:
t_ = std::thread([*this](){threadProc();});