Details
-
Bug
-
Resolution: Fixed
-
P2: Important
-
5.12.5
-
None
-
-
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(); } };