QTimer: delete pending single shot timers when receiver's thread dies

QSingleShotTimer instances are usually destroyed when they receive their
QTimerEvent, and otherwise are children of the creating thread's event
dispatcher so that they do get destroyed even if they never fired.

However, if the receiver object lives in a different thread than the
thread that started the timer, then we move the QSST instance to
that thread to make sure that it starts in the right thread. We then
reparented the QSST instance to nullptr (it can't continue to be a
child of the creating thread's event dispatcher), and connected to
QApp::aboutToQuit as a fall-back in case the timer never fires.

This has two problems: if the timer never fires (e.g. because the
receiver's thread stopped before the timeout), then we created a
soft leak (until aboutToQuit). And since the QSST instance was
moved to the receiver's thread, the connection to aboutToQuit()
is always queued, and also never got processed if the thread was
stopped before (especially since we connected to deleteLater(),
which would require another event processing cycle). So in
practice, we ended up with a hard leak.

To fix this, we have to reparent the QSST instance to the event
dispatcher of the receiver's thread. We can do that reliably
once the receiver thread processes events. Simplify the code to
replace a meta-call with a posted event to ourselves, receiving
which starts the timer (or immediately fires timeout if it has
taken long enough to get there). To avoid memory leaks in the
unlikely case that this posted event never gets delivered (e.g.
because the thread is shutting down already), give that event
ownership of the QSST instance until the timer is started.

This turns out to be leaking anyway on most platforms, which
requires further investigation.

To be able to reparent safely away from a parent in a different
thread, clear the sendChildEvents flag first.

Amends 87535e4e4337596066258e361143cb9906e89512.

Conflict resolution in 6.8: adapt to usage of raw timer IDs,
as 5e36b9e929bb62a96cfe8d16bcc23d6a45d8294b is only in 6.9 and
later.

Fixes: QTBUG-135636
Change-Id: I8188160d54cfb63cb1765c5de8a6c0728dabb7e5
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
(cherry picked from commit 96ef0004111b47cec239846b169942bbc885c181)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit cb199ab2aabb57c2825a042ab398679f3804e940)
This commit is contained in:
Volker Hilsheimer 2025-04-06 18:32:46 +02:00
parent 23e902a4ac
commit 983bb1f225
3 changed files with 136 additions and 20 deletions

View File

@ -7,6 +7,7 @@
#include "qcoreapplication.h"
#include "qmetaobject_p.h"
#include "private/qnumeric_p.h"
#include "qthread.h"
QT_BEGIN_NAMESPACE
@ -45,28 +46,22 @@ void QSingleShotTimer::startTimerForReceiver(Duration interval, Qt::TimerType ti
const QObject *receiver)
{
if (receiver && receiver->thread() != thread()) {
// Avoid leaking the QSingleShotTimer instance in case the application exits before the
// timer fires
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
&QObject::deleteLater);
QObjectPrivate *d_ptr = QObjectPrivate::get(this);
d_ptr->sendChildEvents = false;
setParent(nullptr);
moveToThread(receiver->thread());
QDeadlineTimer deadline(interval, timerType);
auto invokable = [this, deadline, timerType] {
if (deadline.hasExpired()) {
Q_EMIT timeout();
} else {
timerId = Qt::TimerId{startTimer(deadline.remainingTimeAsDuration(), timerType)};
}
};
QMetaObject::invokeMethod(this, invokable, Qt::QueuedConnection);
QCoreApplication::postEvent(this,
new StartTimerEvent(this, QDeadlineTimer(interval, timerType)));
// the event owns "this" and is handled concurrently, so unsafe to
// access "this" beyond this point
} else {
timerId = Qt::TimerId{startTimer(interval, timerType)};
}
}
void QSingleShotTimer::timerEvent(QTimerEvent *)
void QSingleShotTimer::timerFinished()
{
// need to kill the timer _before_ we emit timeout() in case the
// slot connected to timeout calls processEvents()
@ -74,13 +69,28 @@ void QSingleShotTimer::timerEvent(QTimerEvent *)
killTimer(std::exchange(timerId, Qt::TimerId::Invalid));
Q_EMIT timeout();
// we would like to use delete later here, but it feels like a
// waste to post a new event to handle this event, so we just unset the flag
// and explicitly delete...
delete this;
}
void QSingleShotTimer::timerEvent(QTimerEvent *event)
{
if (event->id() == Qt::TimerId::Invalid) {
StartTimerEvent *startTimerEvent = static_cast<StartTimerEvent *>(event);
startTimerEvent->timer.release();
const QDeadlineTimer &deadline = startTimerEvent->deadline;
if (deadline.hasExpired()) {
timerFinished();
} else {
timerId = Qt::TimerId{startTimer(deadline.remainingTimeAsDuration(),
deadline.timerType())};
// we are now definitely in a thread that has an event dispatcher
setParent(QThread::currentThread()->eventDispatcher());
}
} else {
timerFinished();
}
}
QT_END_NAMESPACE
#include "moc_qsingleshottimer_p.cpp"

View File

@ -18,7 +18,8 @@
#include <QtCore/qobject.h>
#include <QtCore/qabstracteventdispatcher.h>
#include <QtCore/qnamespace.h>
#include <QtCore/qcoreevent.h>
#include <QtCore/qdeadlinetimer.h>
QT_BEGIN_NAMESPACE
@ -26,7 +27,7 @@ namespace QtPrivate {
class QSlotObjectBase;
}
class QSingleShotTimer : public QObject
class Q_AUTOTEST_EXPORT QSingleShotTimer : public QObject
{
Q_OBJECT
@ -48,6 +49,18 @@ Q_SIGNALS:
void timeout();
private:
class StartTimerEvent : public QTimerEvent
{
public:
StartTimerEvent(QSingleShotTimer *timer, const QDeadlineTimer &deadline)
: QTimerEvent(Qt::TimerId::Invalid), timer(timer), deadline(deadline)
{}
std::unique_ptr<QSingleShotTimer> timer;
QDeadlineTimer deadline;
};
void timerFinished();
void timerEvent(QTimerEvent *) override;
};

View File

@ -23,6 +23,7 @@
#include <QSignalSpy>
#include <QtTest/private/qpropertytesthelper_p.h>
#include <QtCore/private/qsingleshottimer_p.h>
#include <qtimer.h>
#include <qthread.h>
#include <qelapsedtimer.h>
@ -84,6 +85,10 @@ private slots:
void singleShot_static();
void crossThreadSingleShotToFunctor_data();
void crossThreadSingleShotToFunctor();
#ifdef QT_BUILD_INTERNAL
void crossThreadSingleShotDestruction_data();
void crossThreadSingleShotDestruction();
#endif
void timerOrder();
void timerOrder_data();
void timerOrderBackgroundThread();
@ -1236,6 +1241,94 @@ void tst_QTimer::crossThreadSingleShotToFunctor()
QCOMPARE(DummyFunctor::callThread, &t);
}
#ifdef QT_BUILD_INTERNAL
class SingleShotReceiver : public QObject
{
Q_OBJECT
public:
SingleShotReceiver() = default;
bool timeout = false;
public slots:
void timerFired() {
timeout = true;
thread()->quit();
}
};
void tst_QTimer::crossThreadSingleShotDestruction_data()
{
QTest::addColumn<std::chrono::milliseconds>("timeout");
QTest::addColumn<std::chrono::milliseconds>("threadWaitTime");
// No point in testing zero timeouts, we don't create a QSingleShotTimer
// for those anyway.
QTest::addRow("1ms") << 1ms << 10ms;
QTest::addRow("1s") << 1000ms << 10ms;
}
void tst_QTimer::crossThreadSingleShotDestruction()
{
QFETCH(std::chrono::milliseconds, timeout);
QFETCH(std::chrono::milliseconds, threadWaitTime);
static bool deadTimerDestroyed;
class SingleShotTimer : public QSingleShotTimer
{
public:
using QSingleShotTimer::QSingleShotTimer;
~SingleShotTimer() override { deadTimerDestroyed = true; }
};
{
QThread t;
std::unique_ptr<SingleShotReceiver> receiver(new SingleShotReceiver());
receiver->moveToThread(&t);
// create the timer before the thread is running. Otherwise, the
// assignment to QPointer might happen after the timer already fired and
// destroyed itself, leaving QPointer dangling.
QPointer<QSingleShotTimer> timer = new QSingleShotTimer(timeout, Qt::CoarseTimer,
receiver.get(), SLOT(timerFired()));
t.start();
const bool threadQuit = t.wait(threadWaitTime);
// If we waited long enough, then the timer should have fired, quit
// the thread, and self-destructed; otherwise the wait will have timed
// out and the timer is still alive.
QCOMPARE(threadQuit, receiver->timeout);
QCOMPARE(threadQuit, !timer);
deadTimerDestroyed = threadQuit;
if (!threadQuit) {
t.quit();
// now that the thread is definitely dead, start a new timer that is
// never even going to get started. It should still be destroyed, as
// it will be (temporarily) owned by the event that we post to start
// the timer in the target thread.
// Note: we can't use QPointer here, as the thread is already
// running.
(void)new SingleShotTimer(timeout, Qt::CoarseTimer, receiver.get(), SLOT(timerFired()));
QVERIFY(t.wait());
// the timer should never have fired
QVERIFY(!receiver->timeout);
}
// the timer objects should be destroyed by now in all cases
QVERIFY(!timer);
}
# if defined(Q_OS_DARWIN) || defined(Q_OS_WIN) || defined(Q_OS_QNX) || defined(DISABLE_GLIB)|| defined(QT_NO_GLIB) || defined(QT_GUI_LIB)
QEXPECT_FAIL("1s", "Events posted to a thread after event loop exit are leaking.",
Continue);
# endif
QVERIFY(deadTimerDestroyed);
}
#endif
void tst_QTimer::callOnTimeout()
{
QTimer timer;