c++ - ¿Cómo destruir de forma segura un QThread?
(2)
Quiero destruir correctamente un QThread
en Qt 5.3.
Hasta ahora tengo:
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();
}
Mi problema es que al final del día aún recibo:
QThread: Destruido mientras el hilo todavía se está ejecutando
El hilo seguro
En C ++, el diseño adecuado de una clase es tal que la instancia se puede destruir de forma segura en cualquier momento. Casi todas las clases de Qt actúan de esa manera, pero QThread
no lo hace.
Aquí está la clase que deberías usar en su lugar:
// 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();
}
Se comportará apropiadamente.
Los miembros de QObject no necesitan estar en el montón
Otro problema es que el objeto Worker
se filtrará. En lugar de poner todos esos objetos en el montón, simplemente conviértelos en miembros de MyClass
o su PIMPL.
El orden de las declaraciones de los miembros es importante , ya que los miembros serán destruidos en el orden inverso de la declaración . Por lo tanto, el destructor de MyClass
invocará en orden:
m_workerThread.~Thread()
En este punto, el hilo ha terminado y se ha acabado, ym_worker.thread() == 0
.m_worker.~Worker
Dado que el objeto no tiene hilos, es seguro destruirlo en cualquier hilo.~QObject
Por lo tanto, con el trabajador y su hilo como miembros de 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();
}
};
Y, si no quiere que la implementación esté en la interfaz, lo mismo con 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 Member Parentage
Ahora vemos surgir una regla dura en cuanto a la filiación de cualquier miembro de QObject
. Solo hay dos casos:
Si un miembro de
QObject
no se mueve a otro hilo desde dentro de la clase, debe ser un descendiente de la clase.De lo contrario, debemos mover el miembro de
QObject
a otro hilo. El orden de las declaraciones de los miembros debe ser tal que el hilo se destruya antes que el objeto. Si no es válido para destruir un objeto que reside en otro hilo.
Solo es seguro destruir un QObject
si se cumple la siguiente afirmación:
Q_ASSERT(!object->thread() || object->thread() == QThread::currentThread())
Un objeto cuyo hilo ha sido destruido se convierte en threadless, y !object->thread()
mantiene.
Uno podría argumentar que no "intentamos" que nuestra clase se mueva a otro hilo. Si es así, entonces obviamente nuestro objeto ya no es un objeto QObject
, ya que un QObject
tiene el método moveToThread
y se puede mover en cualquier momento. Si una clase no obedece el principio de sustitución de Liskov a su clase base, es un error reclamar la herencia pública de la clase base. Por lo tanto, si nuestra clase hereda públicamente de QObject
, debe permitir que se mueva a cualquier otro tema en cualquier momento.
El QWidget
es un poco atípico a este respecto. Como mínimo, debería haber hecho que moveToThread
un método protegido.
Por ejemplo:
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 () no detiene el hilo instantáneamente, es solo una forma de indicarle a su código de ejecución que finalice limpiamente (debe verificar isInterruptionRequested () y detener el cálculo usted mismo).
De Qt docs:
Solicite la interrupción del hilo. Esa solicitud es consultiva y depende del código que se ejecuta en el hilo para decidir si debe actuar al recibir dicha solicitud y cómo debe hacerlo. Esta función no detiene ningún bucle de evento que se ejecute en el hilo y no lo termina de ninguna manera. Consulte también isInterruptionRequested ().