C++ 构造函数中使用 this 指针

28
在 C++ 中,在类构造函数期间,我使用 this 指针启动了一个新的线程,并且该线程会广泛使用该指针(例如,调用成员函数)。这样做有什么不好的影响吗?为什么?
我的线程启动过程在构造函数的末尾。
7个回答

21

结果是线程可以启动并开始执行尚未完全初始化的对象。这本身就很糟糕。

如果你认为“好了,这将是构造函数中的最后一句话,它将几乎构建完成......”请再想一想:你可能从该类派生,而派生对象将无法构建。

编译器可能想在你的代码中玩耍,并决定重新排列指令,它可能会在执行代码的任何其他部分之前通过this指针...多线程是棘手的。


1
如果B从A派生,即使您在A的构造函数末尾启动线程,B的所有内容(vtable、成员变量、构造函数代码)都不会被初始化/执行。这可能是一个令人讨厌的并发错误吗?乍一看,我会说是的,因为对象将在构造线程中更改,而它可能会在新创建的线程中使用。 - paercebal

4

主要的后果是线程可能在构造函数完成之前开始运行(并使用您的指针),因此对象可能处于未定义/不可用状态。同样,根据线程停止的方式,它可能会在析构函数开始后继续运行,因此对象再次可能处于不可用状态。

如果您的类是一个基类,则特别麻烦,因为派生类构造函数直到您的构造函数退出后才开始运行,而派生类析构函数将在您的析构函数开始之前完成。此外,在派生类构造之前和析构之后,虚函数调用并不像您想象的那样:虚调用“忽略”其对象部分不存在的类。

例子:

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()函数,用户在构造后调用该函数即可。

1
基本上,您需要的是两阶段构造: 您希望仅在对象完全构造后才启动线程。John Dibling answered 昨天回答了一个类似(但不是重复的)问题,并详细讨论了两阶段构造。您可能想看一下。
然而,请注意,这仍然存在一个问题,即线程可能会在派生类构造函数完成之前启动。(派生类的构造函数在其基类之后调用。)
因此,最安全的方法可能就是手动启动线程:
class Thread { 
  public: 
    Thread();
    virtual ~Thread();
    void start();
    // ...
};

class MyThread : public Thread { 
  public:
    MyThread() : Thread() {}
    // ... 
};

void f()
{
  MyThread thrd;
  thrd.start();
  // ...
}

1

这取决于您在启动线程后执行的操作。如果您在线程启动后执行初始化工作,则它可能使用未正确初始化的数据。

您可以通过使用首先创建对象,然后启动线程的工厂方法来降低风险。

但我认为设计中最大的缺陷是,对我来说,一个构造函数做的不仅仅是“构造”似乎相当令人困惑。


1

这可能存在潜在危险。

在基类构造期间,任何对虚函数的调用都不会分派到更多派生类中尚未完全构造的重写函数。一旦更深层次的类的构造完成,情况就会改变。

如果你启动的线程调用了虚函数,而无法确定它在类构造完成之前或之后发生,那么你很可能会得到不可预测的行为;也许会崩溃。

如果没有虚函数,如果线程仅使用已经完全构造的类部分的方法和数据,则其行为可能是可预测的。


1

我认为,一般情况下,你应该避免这样做。但在许多情况下,你肯定可以这样做。我认为基本上有两件事可能会出错:

  1. 新线程可能在构造函数完成初始化之前尝试访问对象。你可以通过确保所有初始化完成后再启动线程来解决这个问题。但是如果有人从你的类继承呢?你无法控制他们的构造函数会做什么。
  2. 如果线程启动失败会发生什么?没有真正干净的方法在构造函数中处理错误。你可以抛出异常,但这很危险,因为这意味着你的对象的析构函数不会被调用。如果你选择不抛出异常,则必须在各种方法中编写代码以检查是否正确地进行了初始化。

一般来说,如果你需要执行复杂、容易出错的初始化操作,最好在方法中执行,而不是在构造函数中执行。


如果您的类直接处理原始资源,而没有在构造失败时调用dtor,则只有危险。无论如何,它都不应该这样做。由于异常是报告构造失败的唯一方法,因此它们肯定不应该被禁止,以允许松散实现的类不泄漏。尽管如此,我同意您所说的一切。 - sbi
我并没有说它应该被禁止,只是说它很危险。在异常情况下,需要一定程度的小心确保所有东西都被正确清理。当你的类依赖于你没有编写和控制的代码时,这可能特别困难。 - Peter Ruderman

0

没问题,只要你能立即开始使用指针。如果你需要在新线程可以使用指针之前完成初始化的其余构造函数,则需要进行一些同步。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接