初始化类构造函数中的线程会导致崩溃吗?

时间:2015-11-06 17:03:42

标签: c++ multithreading stl

我无法确定一个奇怪的崩溃来自哪里,但事实上它没有发生确定性使我怀疑线程。

我有这样的事情:

class MyClass
{
 MyClass() : mExit(false), mThread(&MyClass::ThreadMain,this)
 {}

 void ThreadMain()
 {
  unique_lock<mutex> lock(mMutex);
  mCondition.wait(lock, [&] { return mExit; });
 }

 std::thread mThread;
 std::mutex mMutex;
 std::condition_variable mCondition;
 bool mExit;
};

显然这是非常简化但我不确定崩溃发生在哪里,所以我想问一下这个设置是否会导致问题?例如,初始化的所有内容是什么顺序 - 例如,在类的实例完全构造之前是否有可能运行ThreadMain

它看起来像我在网上看过的一些例子,但我不确定它是否绝对安全。

3 个答案:

答案 0 :(得分:8)

我看到的唯一问题是类成员按照它们在类中声明的顺序进行初始化。由于mThread位于所有其他类成员之前,因此线程可能在初始化之前使用它们。

要解决此问题,您可以重新安排班级成员,但我不喜欢这种方法。如果其他人出现并更改了订单,则可能会破坏代码。您应该能够让线程初始化默认值,然后在构造函数体中启动线程,因为此时所有类成员都已初始化。

答案 1 :(得分:2)

除了@NathanOliver描述的member-construction-order-vs-thread-early-execution问题之外,我想指出在使用虚拟函数时代码仍然会出现未定义的行为。 ThreadMain的地方。

在设计中使用虚函数是一个问题,因为从vtable中查找虚函数,并且初始化指向vtable的指针,直到构造函数块完成执行。因此,您最终得到一个线程,该线程使用指向尚未初始化的函数的指针,即UB。

这种RAII线程处理程序问题的一般解决方案是使用start函数将对象的初始化与线程的执行例如分开。这也将消除对成员构造顺序的依赖。

struct MyClass {
    MyClass() : mExit(false) {}
    void start() { mThread = std::thread{&ThreadMain, this}; } // Start function.
    virtual void ThreadMain() = 0;

    std::atomic<bool> mExit; // Not even bool is atomic :)
    std::mutex mMutex;
    std::condition_variable mCondition;
    std::thread mThread;
};

这可确保在启动线程时构造MyClass。现在,也可以使用多态。

struct Derived : public MyClass {
    virtual void ThreadMain() {
        std::unique_lock<std::mutex> lock(mMutex);
        mCondition.wait(lock, [&] { return mExit.load(); });
    }
};

但是,现在必须使用两个语句而不是一个例如MyClass m; m.start();来启动线程。为了解决这个问题,我们可以简单地创建一个在构造函数体中执行start函数的包装类。

struct ThreadHandler {
    ThreadHandler() { d.start(); }
    Derived d;
};

答案 2 :(得分:1)

是的,它可能有不良行为,因为mThread实例尚未构建时可能会启动MyClass

我的拇指规则:如果我必须在构造函数中使用this,我会做一些顽皮的事情;)。