【发布时间】:2010-03-25 15:40:55
【问题描述】:
在C++ 中,在类构造过程中,我以this 指针作为参数启动了一个新线程,该参数将在线程中广泛使用(例如,调用成员函数)。这是一件坏事吗?为什么以及后果是什么?
我的线程启动过程在构造函数的末尾。
【问题讨论】:
标签: c++ constructor multithreading this
在C++ 中,在类构造过程中,我以this 指针作为参数启动了一个新线程,该参数将在线程中广泛使用(例如,调用成员函数)。这是一件坏事吗?为什么以及后果是什么?
我的线程启动过程在构造函数的末尾。
【问题讨论】:
标签: c++ constructor multithreading this
结果是线程可以启动并且代码将开始执行尚未完全初始化的对象。这本身就已经够糟糕的了。
如果您正在考虑“好吧,它将是构造函数中的最后一句话,它将与构造函数一样......”再想一想:您可能从该类派生,而派生对象将不被建造。
编译器可能想玩弄你的代码并决定它会重新排序指令,它实际上可能会在执行代码的任何其他部分之前传递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;
}
现在,如果您创建 DerivedThread,最好在构造它的线程和新线程之间进行竞赛,以确定调用 id() 的哪个版本。可能会发生更糟糕的事情,您需要非常仔细地查看您的线程 API 和编译器。
不必担心这一点的通常方法是给你的线程类一个start()函数,用户在构造它之后调用它。
【讨论】:
取决于您在启动线程后执行的操作。如果您在线程启动后执行初始化工作,那么它可能会使用未正确初始化的数据。
您可以通过使用首先创建对象,然后启动线程的工厂方法来降低风险。
但我认为设计中最大的缺陷是,至少对我来说,一个不仅仅是“构造”的构造函数似乎很混乱。
【讨论】:
这可能有潜在的危险。
在基类的构造过程中,任何对虚函数的调用都不会发送到尚未完全构造的更多派生类中的覆盖;一旦更多派生类的构造改变了这种变化。
如果您启动的线程调用了一个虚函数,并且与类的构建完成相关的情况是不确定的,那么您可能会得到不可预知的行为;可能是崩溃。
如果没有虚函数,如果线程只使用类中已完全构造的部分的方法和数据,则行为可能是可预测的。
【讨论】:
我想说,作为一般规则,您应该避免这样做。但在许多情况下,你当然可以侥幸逃脱。我认为基本上有两件事会出错:
一般来说,如果您要执行复杂且容易出错的初始化,那么最好在方法而不是构造函数中执行。
【讨论】:
基本上,您需要的是两阶段构造:您只想在对象完全构造之后启动线程。 John Dibling answered 昨天一个类似的(不是重复的)问题详尽地讨论了两阶段的建设。你可能想看看它。
但是请注意,这仍然会留下线程可能在派生类的构造函数完成之前启动的问题。 (派生类的构造函数在其基类的构造函数之后调用。)
所以说到底最保险的大概就是手动启动线程了:
class Thread {
public:
Thread();
virtual ~Thread();
void start();
// ...
};
class MyThread : public Thread {
public:
MyThread() : Thread() {}
// ...
};
void f()
{
MyThread thrd;
thrd.start();
// ...
}
【讨论】:
没关系,只要您可以立即开始使用该指针即可。如果您要求构造函数的其余部分在新线程可以使用指针之前完成初始化,那么您需要进行一些同步。
【讨论】:
有些人认为你不应该在构造函数中使用this 指针,因为对象还没有完全形成。但是,如果您小心的话,您可以在构造函数中(在 {body} 甚至在初始化列表中)使用它。
这是始终有效的:构造函数(或从构造函数调用的函数)的{body} 可以可靠地访问在基类中声明的数据成员和/或在构造函数自己的类中声明的数据成员。这是因为所有这些数据成员都保证在构造函数的 {body} 开始执行时已经完全构造完毕。
这里有一些东西永远不会起作用:构造函数(或从构造函数调用的函数)的 {body} 不能通过调用在派生类中被覆盖的虚拟成员函数来得到派生类。如果您的目标是获取派生类中的覆盖函数,那么您将得不到您想要的。请注意,无论您如何调用虚成员函数,您都不会在派生类中获得覆盖:显式使用 this 指针(例如,this->method()),隐式使用 this 指针(例如,method( )),甚至调用一些其他函数来调用 this 对象上的虚拟成员函数。底线是:即使调用者正在构造派生类的对象,在基类的构造函数期间,您的对象还不属于该派生类。您已收到警告。
这里有时会起作用:如果您将此对象中的任何数据成员传递给另一个数据成员的初始化程序,则必须确保其他数据成员已被初始化。好消息是,您可以使用一些直接的语言规则来确定其他数据成员是否已经(或尚未)初始化,这些规则与您正在使用的特定编译器无关。坏消息是您必须知道那些语言规则(例如,首先初始化基类子对象(如果您有多重继承和/或虚拟继承,请查找顺序!),然后在类中定义的数据成员在它们出现在类声明中的顺序)。如果您不知道这些规则,那么不要将任何数据成员从 this 对象(无论您是否明确使用 this 关键字)传递给任何其他数据成员的初始化程序!如果你知道规则,请小心。
【讨论】: