A simple solution is to prevent the problem: if the object is about to become threadless, move it to the thread handle's parent thread, and then when the thread itself is about to be destructed, reestablish the object's timers to prevent the warning.
QObject
's moveToThread
implementation has two parts:
The QEvent::ThreadChange
is delivered to the object from moveToThread
. QObject::event
uses this event to capture and deactivate the timers active on the object. Those timers are packaged in a list and posted to the object's internal _q_reactivateTimers
method.
The event loop in the destination thread delivers the metacall to the object, the _q_reregisterTimers
runs in the new thread and the timers get reactivated in the new thread. Note that if _q_reregisterTimers
doesn't get a chance to run, it will irrevocably leak the timer list.
Thus we need to:
Capture the moment the object is about to become threadless, and move it to a different thread, so that the QMetaCallEvent
to _q_reactivateTimers
won't be lost.
Deliver the event in the correct thread.
And so:
// https://github.com/KubaO/stackoverflown/tree/master/questions/qbasictimer-stop-fix-50636079
#include <QtCore>
class Thread final : public QThread {
Q_OBJECT
void run() override {
connect(QAbstractEventDispatcher::instance(this),
&QAbstractEventDispatcher::aboutToBlock,
this, &Thread::aboutToBlock);
QThread::run();
}
QAtomicInt inDestructor;
public:
using QThread::QThread;
/// Take an object and prevent timer resource leaks when the object is about
/// to become threadless.
void takeObject(QObject *obj) {
// Work around to prevent
// QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
static constexpr char kRegistered[] = "__ThreadRegistered";
static constexpr char kMoved[] = "__Moved";
if (!obj->property(kRegistered).isValid()) {
QObject::connect(this, &Thread::finished, obj, [this, obj]{
if (!inDestructor.load() || obj->thread() != this)
return;
// The object is about to become threadless
Q_ASSERT(obj->thread() == QThread::currentThread());
obj->setProperty(kMoved, true);
obj->moveToThread(this->thread());
}, Qt::DirectConnection);
QObject::connect(this, &QObject::destroyed, obj, [obj]{
if (!obj->thread()) {
obj->moveToThread(QThread::currentThread());
obj->setProperty(kRegistered, {});
}
else if (obj->thread() == QThread::currentThread() && obj->property(kMoved).isValid()) {
obj->setProperty(kMoved, {});
QCoreApplication::sendPostedEvents(obj, QEvent::MetaCall);
}
else if (obj->thread()->eventDispatcher())
QTimer::singleShot(0, obj, [obj]{ obj->setProperty(kRegistered, {}); });
}, Qt::DirectConnection);
obj->setProperty(kRegistered, true);
}
obj->moveToThread(this);
}
~Thread() override {
inDestructor.store(1);
requestInterruption();
quit();
wait();
}
Q_SIGNAL void aboutToBlock();
};
int main(int argc, char *argv[]) {
static_assert(QT_VERSION < QT_VERSION_CHECK(5,11,0), "");
QCoreApplication app(argc, argv);
QObject object1, object2;
object1.startTimer(10);
object2.startTimer(200);
Thread workThread1, workThread2;
QTimer::singleShot(500, &QCoreApplication::quit);
workThread1.start();
workThread2.start();
workThread1.takeObject(&object1);
workThread2.takeObject(&object2);
app.exec();
}
#include "main.moc"
This approach can be easily extended to dynamically track all children of obj
as well: Qt provides sufficient events to do such tracking.