How to prevent the QBasicTimer::stop: Failed warning when objects become threadless?
Asked Answered
E

3

7

QObjects can easily become threadless, when their work thread finishes ahead of them. When this happens, Qt doesn't release their timer ids, even though the timers are not active anymore. Thus, a QBasicTimer::stop: Failed. Possibly trying to stop from a different thread warning appears. It has mostly cosmetic consequences, but does indicate a timer id leak, and thus a workaround would be nice to have. The following example triggers the problem:

#include <QtCore>
int main(int argc, char *argv[]) {
   static_assert(QT_VERSION < QT_VERSION_CHECK(5,11,0), "");
   QCoreApplication app(argc, argv);
   QObject object;
   object.startTimer(1000);
   QThread workThread;
   workThread.start();
   object.moveToThread(&workThread);
   QTimer::singleShot(500, &QCoreApplication::quit);
   app.exec();
   workThread.quit();
   workThread.wait();
}

It'd be nice if the workaround didn't have to make any modifications to how the timers are allocated, i.e. that there would be no extra tracking of timers needed beyond what Qt already does.

Eolithic answered 1/6, 2018 at 4:24 Comment(0)
E
3

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

Eolithic answered 1/6, 2018 at 5:8 Comment(0)
C
1

Hold the timer id to be killed from within thread - by object:

 int id = object.startTimer(1000);
 QThread workThread;
 workThread.start();
 object.moveToThread(&workThread);
 QTimer::singleShot(500, &QCoreApplication::quit);
 QObject::connect(&workThread, &QThread::finished, [&](){object.killTimer(id);});

...
Codification answered 1/6, 2018 at 5:6 Comment(2)
That's of course fine, but the whole point is not to mess with it directly. Assume that you have potentially dozens of timers, spread across several classes, most of which encapsulate their timers and provide no direct access to them.Affairs
I don't understand the downvote .. the answer is not wrongCodification
S
-1

How about moving the object back to the main thread...

class Object : public QObject
{
public:
    using QObject::QObject;
    virtual ~Object() {
        qDebug()<<"Object"<<QThread::currentThread()<<this->thread();
        if(thread() == Q_NULLPTR)
            moveToThread(QThread::currentThread());
    }
};

#include <QtCore>
int main(int argc, char *argv[]) {
   static_assert(QT_VERSION < QT_VERSION_CHECK(5,11,0), "");
   QCoreApplication app(argc, argv);
   Object object;
   object.startTimer(1000);
   QThread workThread;
   workThread.start();
   object.moveToThread(&workThread);
   QTimer::singleShot(500, &QCoreApplication::quit);
   qDebug()<<"main"<<QThread::currentThread()<<object.thread();
   app.exec();
   workThread.quit();
   workThread.wait();
}
Seaweed answered 1/6, 2018 at 9:35 Comment(1)
That doesn't always work, unfortunately. By the time the thread is null, the timer ids can be already leaked. It depends a bit on circumstances.Affairs

© 2022 - 2024 — McMap. All rights reserved.