我怎样才能让 QThread 在不泄漏的情况下发出堆分配的 QObject?

How can I have QThread emit a heap-allocated QObject without leaking?

我的情况是我有一个 QWidget 派生的 class,MyWidget,它将创建一个 QThread 派生的 class (WorkerThread) 来在其 运行() 方法。其结果是 QObject 派生的 class (DataClass) 的堆分配实例,然后由 MyWidget 接收和处理。不过,MyWidget 是一个临时小部件,可能会在 WorkerThread 仍然 运行ning 时因用户操作而被删除。

这里有一些伪代码来说明这一点:

#include <QThread>
#include <QWidget>

class DataClass : public QObject {
    Q_OBJECT
public:
    // contains some complex data
};

class WorkerThread : public QThread {
    Q_OBJECT
public:
    virtual void run() {
        DataClass *result = new DataClass;
        doSomeReallyLongUninterruptibleWork(result);
        emit workComplete(result);
    }
signals:
    void workComplete(DataClass *);
};

class MyWidget : public QWidget {
    Q_OBJECT
public:
    void doBlockingWork() {
        WorkerThread *worker = new WorkerThread;
        connect(worker, &WorkerThread::finished, worker, &WorkerThread::deleteLater);
        connect(worker, &WorkerThread::workComplete, this, &MyWidget::processData);
        worker->start();
    }

public slots:
    void processData(DataClass *result) {
        // Do some stuff
        delete result;
        // Assuming MyWidget still exists when WorkerThread has finished, no memory has leaked
    }
};

通常,return 工作线程的结果的正确 "Qt" 方法是让它发出一个信号,其参数是其工作的结果,如上图所示。这对于可以复制的数据来说很好,但由于结果是指向堆分配对象的指针,我必须小心确保内存被释放。

通常不会有问题,因为既然 WorkerThread 已经完成,我可以安全地将指向 DataClass 的指针传递给 MyWidget,让它处理 DataClass,然后释放它。

问题是,正如我之前所说,MyWidget 是暂时的,可能会在 WorkerThread 完成之前被销毁。在这种情况下,如何确保 DataClass 的实例以某种方式被释放?

特别是,我正在寻找对他们来说有一些优雅的解决方案,这意味着它利用了 Qt 的功能,并且最好使它使 WorkerThread 保持与 MyWidget 的分离,这样 WorkerThread 就不需要知道关于它的任何内容或可能创建它的任何其他 class。我也乐于接受改进我已经使用的模式的想法。

使用智能指针(例如,QSharedPointer)代替普通指针:

DataClass *result = new DataClass;

应替换为

QSharedPointer<DataClass> result = QSharedPointer<DataClass>(new DataClass);

然后,您就可以安全地将它传递到某个地方,而不必担心删除它。当它超出最后一个可以使用的范围时,该对象将被自动销毁。

worker 应该将结果推送到主线程,以表明在那里使用是安全的(根据 QObject 语义)。在通知每个感兴趣的人工作完成后,结果应该在主线程中自动删除。这是一个最小的变化:

void run() override {
   auto result = new DataClass;
   doSomeReallyLongUninterruptibleWork(result);
   result->moveToThread(qApp->thread());   // added
   emit workComplete(result);
   QObject::connect(this, &QThread::finished, result, &QObject::deleteLater); // added
}

你保证 deleteLater 将在 workComplete 的最后一个处理程序在主线程中完成后被调用。

主线程中的单个对象可能希望将结果保留更长时间。这可以通过在结果对象上设置父级来指示。那么不应删除该对象:

...
QObject::connect(this, &QThread::finished, result, [result]{
  if (!result->parent()) result->deleteLater();
});

如果你想让主线程中的多个对象保留更长时间的结果,你应该在 workComplete 的参数中使用 QSharedPointer,并且你绝不能设置结果:非空父对象和 QSharedPointer 互不兼容:前者表示父对象的唯一所有权,后者表示共享所有权。

有必要将 DataClass 对象移动到主线程以避免 DataClass::thead() 上的竞争并允许 deleteLater 工作:

  1. 工作线程:emit workComplete(result)
  2. 主线程:开始使用resultresult.thread()是worker实例。
  3. 工作线程:完成
  4. 主线程:result.thread() 现在是 nullptr,而主线程正在使用它。

这可能不是问题,但通常表示设计不佳。一旦您开始使用 DataClass 的更多 QObject 功能,它就会将潜在错误变成真正的错误:例如deleteLater 不工作,定时器不工作等

此外,不支持在其线程以外的任何线程中销毁 QObject。假设您有原始代码。以下情况可能会发生并导致未定义的行为:

  1. 工作线程:emit workComplete(result)
  2. 主线程:开始使用resultresult.thread()是worker实例。
  3. 主线程:delete resultQObject::~QObjectqApp->thread() 中调用,但 result->thread() 是工作线程的不同的、仍然活动的实例。

如果您希望发现此类问题,请添加:

DataClass::~DataClass() {
  Q_ASSERT(thread() == nullptr || thread() == QThread::currentThread());
  ...
}

可以销毁无线程对象,但此类对象的功能并不完整:您不能 deleteLater 它们,它们的计时器不工作,它们不接收事件等。

deleteLater 之前进行父检查的必要性取决于您是否打算将结果的存在时间延长到连接到 workComplete.

的代码之后

"obvious" 共享指针的使用并不能明确哪个线程可以安全地访问结果,前提是结果不是线程安全的。它本身也没有做任何事情来解决这样一个事实,即一旦 worker 完成,QObject 就只有一半功能了,因为没有与之关联的事件循环。我相信您的意图是只有一个线程可以拥有结果,因此它的方法不必是线程安全的。幸运的是,QObject 的语义已经清楚地表达了这一点:对象的 thread() 是被授权对该对象进行操作的人。

主线程中 workComplete 的任何接收者都将在结果消失之前对其进行处理。如果主线程中的任何对象想要获得结果的所有权,它可以 - 通过设置父线程。否则,一旦 workComplete 处理程序完成,如果 none 已声明所有权,结果将从主事件循环中删除。

QTimer::singleShot(1000, w.data(), [&]{ w.reset(); }) 计时器更改为 2500 毫秒,使小部件比工作线程存活更久,并注意行为的差异,具体取决于它是否声明所有权。

完整示例:

// https://github.com/KubaO/Whosebugn/tree/master/questions/worker-shared-37956073
#include <QtCore>

struct DataClass : public QObject {
   DataClass() { qDebug() << __FUNCTION__; }
   ~DataClass() { qDebug() << __FUNCTION__; }
};
void doSomeReallyLongUninterruptibleWork(DataClass*) { QThread::sleep(2); }

class WorkerThread : public QThread {
   Q_OBJECT
public:
   void run() override {
      auto result = new DataClass;
      doSomeReallyLongUninterruptibleWork(result);
      result->moveToThread(qApp->thread());
      emit workComplete(result);
      QObject::connect(this, &QThread::finished, result, [result]{
         if (!result->parent()) {
            qDebug() << "DataClass is unclaimed and will deleteLater";
            result->deleteLater();
         }
      });
   }
   Q_SIGNAL void workComplete(DataClass*);
};

class MyWidget : public QObject {
   void processData(DataClass * result) {
      // Do stuff with result
      // Retain ownership (optional)
      if (true) result->setParent(this);
   }
public:
   void doBlockingWork() {
      auto worker = new WorkerThread;
      connect(worker, &WorkerThread::workComplete, this, &MyWidget::processData);
      connect(worker, &WorkerThread::finished, worker, &WorkerThread::deleteLater);
      worker->start();
   }
   ~MyWidget() { qDebug() << __FUNCTION__; }
};

int main(int argc, char ** argv) {
   QCoreApplication app{argc, argv};
   QScopedPointer<MyWidget> w{new MyWidget};
   w->doBlockingWork();
   QTimer::singleShot(1000, w.data(), [&]{ w.reset(); });
   QTimer::singleShot(3000, qApp, &QCoreApplication::quit);
   return app.exec();
}

#include "main.moc"

您也可以放弃使用显式线程,而改用 QtConcurrent::run。这没有明显的优势,我在这里展示它只是为了表明这两种方法都是可行的。

#include <QtConcurrent>

struct DataClass : public QObject {
   Q_SIGNAL void ready();
   Q_OBJECT
};

// Let's not pollute the default pool with long-running stuff
Q_GLOBAL_STATIC(QThreadPool, longPool)

class MyWidget : public QObject {
   void processData(DataClass * result) {
      // Do stuff with result
      // Retain ownership (optional)
      if (true) result->setParent(this);
   }
public:
   void doBlockingWork() {
      auto result = new DataClass;
      connect(result, &DataClass::ready, this, [=]{ MyWidget::processData(result); });
      result->moveToThread(nullptr);
      QtConcurrent::run(longPool, [result]{
         result->moveToThread(QThread::currentThread());
         doSomeReallyLongUninterruptibleWork(result);
         result->moveToThread(qApp->thread());
         emit result->ready();
         QTimer::singleShot(0, result, [result]{
            if (!result->parent()) result->deleteLater();
         });
      });
   }
};