Как безопасно уничтожить QThread?

Я хочу правильно уничтожить QThread в Qt 5.3.

Пока у меня есть:

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: уничтожено во время работы потока

2 ответа

Решение

Безопасная нить

В 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();
}

Это будет вести себя соответственно.

Члены QObject не должны быть в куче

Другая проблема заключается в том, что 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()
{}

Происхождение члена QObject

Теперь мы видим жесткое правило в отношении происхождения любого QObject члены. Есть только два случая:

  1. Если QObject элемент не перемещен в другой поток из класса, он должен быть потомком класса.

  2. В противном случае мы должны переместить QObject участник в другой теме. Порядок объявлений членов должен быть таким, что поток должен быть уничтожен перед объектом. Если недопустимо уничтожить объект, который находится в другом потоке.

Это безопасно только уничтожить QObject если верно следующее утверждение:

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

Объект, поток которого был разрушен, становится безрезультатным, и !object->thread() держит.

Можно утверждать, что мы не "намерены" переместить наш класс в другой поток. Если так, то, очевидно, наш объект не является QObject больше, так как QObject имеет moveToThread метод и может быть перемещен в любое время. Если класс не подчиняется принципу подстановки Лискова своему базовому классу, было бы ошибкой требовать открытого наследования от базового класса. Таким образом, если наш класс публично наследует от 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() не останавливает поток мгновенно, это всего лишь один из способов дать понять, что ваш работающий код окончательно завершен (вы должны проверить isInterruptionRequested() и остановить вычисления самостоятельно).

Из Qt документов:

Запросить прерывание потока. Этот запрос носит рекомендательный характер, и от кода, работающего в потоке, зависит, будет ли и как он должен реагировать на такой запрос. Эта функция не останавливает цикл обработки событий в потоке и не прерывает его каким-либо образом. Смотрите также isInterruptionRequested().

Другие вопросы по тегам