我的线程启动过程在构造函数的末尾。
结果是线程可以启动并开始执行尚未完全初始化的对象。这本身就很糟糕。
如果你认为“好了,这将是构造函数中的最后一句话,它将几乎构建完成......”请再想一想:你可能从该类派生,而派生对象将无法构建。
编译器可能想在你的代码中玩耍,并决定重新排列指令,它可能会在执行代码的任何其他部分之前通过this
指针...多线程是棘手的。
主要的后果是线程可能在构造函数完成之前开始运行(并使用您的指针),因此对象可能处于未定义/不可用状态。同样,根据线程停止的方式,它可能会在析构函数开始后继续运行,因此对象再次可能处于不可用状态。
如果您的类是一个基类,则特别麻烦,因为派生类构造函数直到您的构造函数退出后才开始运行,而派生类析构函数将在您的析构函数开始之前完成。此外,在派生类构造之前和析构之后,虚函数调用并不像您想象的那样:虚调用“忽略”其对象部分不存在的类。
例子:
struct BaseThread {
MyThread() {
pthread_create(thread, attr, pthread_fn, static_cast<void*>(this));
}
virtual ~MyThread() {
maybe stop thread somehow, reap it;
}
virtual void id() { std::cout << "base\n"; }
};
struct DerivedThread : BaseThread {
virtual void id() { std::cout << "derived\n"; }
};
void* thread_fn(void* input) {
(static_cast<BaseThread*>(input))->id();
return 0;
}
id()
版本。可能会发生更糟糕的情况,您需要仔细查看您的线程API和编译器。start()
函数,用户在构造后调用该函数即可。class Thread {
public:
Thread();
virtual ~Thread();
void start();
// ...
};
class MyThread : public Thread {
public:
MyThread() : Thread() {}
// ...
};
void f()
{
MyThread thrd;
thrd.start();
// ...
}
这取决于您在启动线程后执行的操作。如果您在线程启动后执行初始化工作,则它可能使用未正确初始化的数据。
您可以通过使用首先创建对象,然后启动线程的工厂方法来降低风险。
但我认为设计中最大的缺陷是,对我来说,一个构造函数做的不仅仅是“构造”似乎相当令人困惑。
这可能存在潜在危险。
在基类构造期间,任何对虚函数的调用都不会分派到更多派生类中尚未完全构造的重写函数。一旦更深层次的类的构造完成,情况就会改变。
如果你启动的线程调用了虚函数,而无法确定它在类构造完成之前或之后发生,那么你很可能会得到不可预测的行为;也许会崩溃。
如果没有虚函数,如果线程仅使用已经完全构造的类部分的方法和数据,则其行为可能是可预测的。
我认为,一般情况下,你应该避免这样做。但在许多情况下,你肯定可以这样做。我认为基本上有两件事可能会出错:
一般来说,如果你需要执行复杂、容易出错的初始化操作,最好在方法中执行,而不是在构造函数中执行。
没问题,只要你能立即开始使用指针。如果你需要在新线程可以使用指针之前完成初始化的其余构造函数,则需要进行一些同步。