Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-112162

QTimer::singleShot() can call receiver in wrong thread

    XMLWordPrintable

Details

    • Bug
    • Resolution: Fixed
    • P2: Important
    • 6.2.8, 6.5.1, 6.6.0
    • 5.12.5
    • Core: Event loop
    • None
    • All
    • 4d90c4e74 (dev), fc319a8f8 (dev), 01ddd9ec5 (6.5), d3bc8c79b (6.5), 9390a4451 (tqtc/lts-6.2), 76dcb60ea (tqtc/lts-6.2), 87535e4e4 (dev)

    Description

      QTimer::singleShot(msec, object, slot) behaves differently depending whether using SLOT vs  PointerToMemberFunction|Functor syntax, when the receiver object moves thread between invoking the singleShot and its timeout.

      Note: Found in 5.12.5 - haven't checked for same issue in newer QT versions.

      QTimer::singleShot() documentation states "The function will be run in the thread of context/receiver"

      For old SLOT() syntax, the slot is correctly called in the context objects thread.

      TEST(QTimer, SingleShotToSLOT_HandlesMoveThreadCorrectly)
      {
          // Given: A singleShot timer is queued to call QObject SLOT() in first thread
          QObjectWithSlot object;
          QTimer::singleShot(1000, &object, SLOT(CheckExpectedThread()));
      
          // When: object is moved to a second thread
          QThreadWithStop secondThread;
          secondThread.start();
          object.moveToThread(&secondThread);
      
          // Then: objects SLOT will be called in the second thread.
          object.m_ExpectedThread = &secondThread;
          QCoreApplication::exec();
          EXPECT_TRUE(object.m_SlotCalled);
      }
      

      But for QT5 PointerToMemberFunction|Functor syntax, the method gets incorrectly called in the original thread the object was in at the time the singleShot was invoked.

      TEST(QTimer, SingleShotToSlotObj_DoesNotHandleMoveThreadCorrectly)
      {
          // Given: A singleShot timer is queued to call &QObject::method in first thread
          QObjectWithSlot object;
          QTimer::singleShot(1000, &object, &QObjectWithSlot::CheckExpectedThread);
      
          // When: object is moved to another thread
          QThreadWithStop secondThread;
          secondThread.start();
          object.moveToThread(&secondThread);
      
          // Then: objects method should be called in the second thread.
          object.m_ExpectedThread = &secondThread;
      #if (QT_VERSION <= QT_VERSION_CHECK(5,12,5))    
          // However: Due to QT bug, objects method will be incorrectly called in the first thread.
          object.m_ExpectedThread = QThread::currentThread();
      #endif    
          QCoreApplication::exec();
          EXPECT_TRUE(object.m_SlotCalled);
      }
      

      Looking through the source code of Qt5.12.5 the singleShot methods ends up creating a QSingleShotTimer with one of 2 constructors depending on Qt4 vs Qt5 syntax.
      From ...\Qt5.12.5\5.12.5\Src\qtbase\src\corelib\kernel\qtimer.cpp

      QSingleShotTimer::QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, const char *member)
          : QObject(QAbstractEventDispatcher::instance()), hasValidReceiver(true), slotObj(0)
      {
          timerId = startTimer(msec, timerType);
          connect(this, SIGNAL(timeout()), r, member);
      }
      
      QSingleShotTimer::QSingleShotTimer(int msec, Qt::TimerType timerType, const QObject *r, QtPrivate::QSlotObjectBase *slotObj)
          : QObject(QAbstractEventDispatcher::instance()), hasValidReceiver(r), receiver(r), slotObj(slotObj)
      {
          timerId = startTimer(msec, timerType);
          if (r && thread() != r->thread()) {
              // Avoid leaking the QSingleShotTimer instance in case the application exits before the timer fires
              connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater);
              setParent(0);
              moveToThread(r->thread());
          }
      }
      

      The string based Qt4 version uses connect which will auto queue to another thread at call time. Whereas the functor based Qt5 version calls slotObj->call() directly from timerEvent() having moved to the receiving thread at time of construction, and because it has no parent it will not move again when the receiver moves thread.

      I assume the setParent(0) is to workaround the "QObject::moveToThread: Cannot move objects with a parent" error.

      Question: why does the QT5 version not just use same method as QT4, but use connect with Qt5 syntax e.g.?

          connect(this, &QTimer::timeout, receiver, slot);
      

       

      The following are just definitions required if building the tests above:

      class QObjectWithSlot : public QObject
      {
      Q_OBJECT;
      public slots:
          void CheckExpectedThread()
          {
              m_SlotCalled = true;
              EXPECT_EQ(QThread::currentThread(), m_ExpectedThread);
              QCoreApplication::quit();
          }
      
      public:    
          QThread* m_ExpectedThread = nullptr;
          bool m_SlotCalled = false;
      };
      
      class QThreadWithStop : public QThread
      {
      public:
          ~QThreadWithStop()
          {
              quit();
              wait();
          }
      };
      

      Attachments

        For Gerrit Dashboard: QTBUG-112162
        # Subject Branch Project Status CR V

        Activity

          People

            thiago Thiago Macieira
            stevenr3 Steve Rossen
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved: