如何安全地销毁QThread

How to safely destruct a QThread?

本文关键字:QThread 安全 何安全      更新时间:2023-10-16

我想在Qt 5.3中正确破坏QThread

到目前为止,我已经得到了:

MyClass::MyClass(QObject *parent) : QObject(parent) {
    mThread = new QThread(this);
    QObject::connect(mThread, SIGNAL(finished()), mThread, SLOT(deleteLater()));
    mWorker = new Worker(); // inherits from QObject
    mWorker->moveToThread(mThread);
    mThread->start();
}
MyClass::~MyClass() {
    mThread->requestInterruption();
}

我的问题是,在一天结束时,我仍然得到:

QThread:在线程仍在运行时销毁

安全线程

在C++中,类的正确设计是可以随时安全地销毁实例。几乎所有的Qt类都是这样工作的,但QThread不是。

以下是您应该改用的类:

// Thread.hpp
#include <QThread>
public Thread : class QThread {
  Q_OBJECT
  using QThread::run; // This is a final class
public:
  Thread(QObject * parent = 0);
  ~Thread();
}
// Thread.cpp
#include "Thread.h"
Thread::Thread(QObject * parent): QThread(parent)
{}
Thread::~Thread() {
  quit();
  #if QT_VERSION >= QT_VERSION_CHECK(5,2,0)
  requestInterruption();
  #endif
  wait();
}

它将表现得适当。

Q 成员不需要在堆上

另一个问题是Worker对象将被泄露。与其将所有这些对象放在堆上,不如简单地使它们成为MyClass或其 PIMPL 的成员。

成员

声明的顺序很重要,因为成员将以声明的相反顺序被破坏。因此,MyClass的析构函数将按以下顺序调用:

  1. m_workerThread.~Thread() 此时线程已完成并消失,m_worker.thread() == 0 .

  2. m_worker.~Worker 由于对象是无线程的,因此在任何线程中销毁它是安全的。

  3. ~QObject

因此,将工作线程及其线程作为MyClass的成员:

class MyClass : public QObject {
  Q_OBJECT
  Worker m_worker;          // **NOT** a pointer to Worker!
  Thread m_workerThread;    // **NOT** a pointer to Thread!
public:
  MyClass(QObject *parent = 0) : QObject(parent),
  // The m_worker **can't** have a parent since we move it to another thread.
  // The m_workerThread **must** have a parent. MyClass can be moved to another
  // thread at any time.
    m_workerThread(this)
  {
    m_worker.moveToThread(&m_workerThread);
    m_workerThread.start();
  }
};

而且,如果您不希望实现在界面中,则使用 PIMPL 也是如此

// MyClass.hpp
#include <QObject>
class MyClassPrivate;
class MyClass : public QObject {
  Q_OBJECT
  Q_DECLARE_PRIVATE(MyClass)
  QScopedPointer<MyClass> const d_ptr;
public:
  MyClass(QObject * parent = 0);
  ~MyClass(); // required!
}
// MyClass.cpp
#include "MyClass.h"
#include "Thread.h"
class MyClassPrivate {
public:
  Worker worker;          // **NOT** a pointer to Worker!
  Thread workerThread;    // **NOT** a pointer to Thread!
  MyClassPrivate(QObject * parent);
};
MyClassPrivate(QObject * parent) :
  // The worker **can't** have a parent since we move it to another thread.
  // The workerThread **must** have a parent. MyClass can be moved to another
  // thread at any time.
    workerThread(parent)
{}
MyClass::MyClass(QObject * parent) : QObject(parent),
  d_ptr(new MyClassPrivate(this))
{
  Q_D(MyClass);
  d->worker.moveToThread(&d->workerThread);
  d->workerThread.start();
}
MyClass::~MyClass()
{}

Q会员亲子关系

我们现在看到,关于任何QObject成员的父母身份,出现了一条硬性规定。只有两种情况:

  1. 如果 QObject 成员未从类中移动到另一个线程,则该成员必须是该类的后代

  2. 否则,我们必须QObject成员移动到另一个线程。成员声明的顺序必须使线程在对象之前被销毁。如果无效,则析构驻留在另一个线程中的对象。

只有当以下断言成立时,销毁QObject才是安全的:

Q_ASSERT(!object->thread() || object->thread() == QThread::currentThread())
线程

已被破坏的对象变为无线程,并且!object->thread()保持。

有人可能会争辩说,我们并不"打算"将我们的类移动到另一个线程。如果是这样,那么显然我们的对象不再是QObject,因为QObject具有moveToThread方法并且可以随时移动。如果一个类不遵守其基类的 Liskov 替换原则,那么从基类声明公共继承是错误的。因此,如果我们的类公开继承自QObject,它必须允许自己随时移动到任何其他线程。

在这方面,QWidget有点异常。至少,它应该使moveToThread成为受保护的方法。

例如:

class Worker : public QObject {
  Q_OBJECT
  QTimer m_timer;
  QList<QFile*> m_files;
  ...
public:
  Worker(QObject * parent = 0);
  Q_SLOT bool processFile(const QString &);
};
Worker::Worker(QObject * parent) : QObject(parent),
  m_timer(this)  // the timer is our child
  // If m_timer wasn't our child, `worker.moveToThread` after construction
  // would cause the timer to fail.
{}
bool Worker::processFile(const QString & fn) {
  QScopedPointer<QFile> file(new QFile(fn, this));
  // If the file wasn't our child, `moveToThread` after `processFile` would
  // cause the file to "fail".
  if (! file->open(QIODevice::ReadOnly)) return false;      
  m_files << file.take();
}

mThread->requestInterruption(( 不会立即停止线程,它只是向正在运行的代码发出干净结束的信号的一种方式(您必须检查 isInterruptionRequest(( 并自己停止计算(。

来自Qt文档:

请求中断线程。该请求是建议性的,由线程上运行的代码决定是否以及如何对此类请求进行操作。此函数不会停止线程上运行的任何事件循环,也不会以任何方式终止它。 另请参阅 isInterruptionRequest((。