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:
parent
23e902a4ac
commit
983bb1f225
@ -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"
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user