QSignalSpy: fix data race between wait() and emit from another thread

Detected by TSAN in tst_QThread::terminateAndPrematureDestruction()
but better have a dedicated unittest, with values emitted by the signal
and recorded in the spy.

Pick-to: 6.5
Change-Id: I141d47b0ea0b63188f8a4f9d056f72df3457bda5
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
(cherry picked from commit c837cd75936cbeeb898dd5808edb9dfaf716a76e)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit 3b428d6f28a3664220d4c7eef8f8ee1779d206e2)
This commit is contained in:
David Faure 2024-03-12 13:18:02 +01:00 committed by Qt Cherry-pick Bot
parent f225d80fee
commit d9f4fc510d
2 changed files with 52 additions and 7 deletions

View File

@ -10,6 +10,7 @@
#include <QtCore/qmetaobject.h> #include <QtCore/qmetaobject.h>
#include <QtTest/qtesteventloop.h> #include <QtTest/qtesteventloop.h>
#include <QtCore/qvariant.h> #include <QtCore/qvariant.h>
#include <QtCore/qmutex.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -43,11 +44,11 @@ public:
return; return;
} }
initArgs(mo->method(sigIndex), obj);
if (!connectToSignal(obj, sigIndex)) if (!connectToSignal(obj, sigIndex))
return; return;
sig = ba; sig = ba;
initArgs(mo->method(sigIndex), obj);
} }
#ifdef Q_QDOC #ifdef Q_QDOC
@ -73,21 +74,23 @@ public:
if (!isSignalMetaMethodValid(signalMetaMethod)) if (!isSignalMetaMethodValid(signalMetaMethod))
return; return;
initArgs(mo->method(sigIndex), obj);
if (!connectToSignal(obj, sigIndex)) if (!connectToSignal(obj, sigIndex))
return; return;
sig = signalMetaMethod.methodSignature(); sig = signalMetaMethod.methodSignature();
initArgs(mo->method(sigIndex), obj);
} }
#endif // Q_QDOC #endif // Q_QDOC
QSignalSpy(const QObject *obj, const QMetaMethod &signal) QSignalSpy(const QObject *obj, const QMetaMethod &signal)
: m_waiting(false) : m_waiting(false)
{ {
if (isObjectValid(obj) && isSignalMetaMethodValid(signal) && if (isObjectValid(obj) && isSignalMetaMethodValid(signal)) {
connectToSignal(obj, signal.methodIndex())) {
sig = signal.methodSignature();
initArgs(signal, obj); initArgs(signal, obj);
if (!connectToSignal(obj, signal.methodIndex()))
return;
sig = signal.methodSignature();
} }
} }
@ -99,10 +102,15 @@ public:
bool wait(std::chrono::milliseconds timeout = std::chrono::seconds{5}) bool wait(std::chrono::milliseconds timeout = std::chrono::seconds{5})
{ {
QMutexLocker locker(&m_mutex);
Q_ASSERT(!m_waiting); Q_ASSERT(!m_waiting);
const qsizetype origCount = size(); const qsizetype origCount = size();
m_waiting = true; m_waiting = true;
locker.unlock();
m_loop.enterLoop(timeout); m_loop.enterLoop(timeout);
locker.relock();
m_waiting = false; m_waiting = false;
return size() > origCount; return size() > origCount;
} }
@ -157,14 +165,17 @@ private:
void initArgs(const QMetaMethod &member, const QObject *obj) void initArgs(const QMetaMethod &member, const QObject *obj)
{ {
QMutexLocker locker(&m_mutex);
args.reserve(member.parameterCount()); args.reserve(member.parameterCount());
for (int i = 0; i < member.parameterCount(); ++i) { for (int i = 0; i < member.parameterCount(); ++i) {
QMetaType tp = member.parameterMetaType(i); QMetaType tp = member.parameterMetaType(i);
if (!tp.isValid() && obj) { if (!tp.isValid() && obj) {
locker.unlock();
void *argv[] = { &tp, &i }; void *argv[] = { &tp, &i };
QMetaObject::metacall(const_cast<QObject*>(obj), QMetaObject::metacall(const_cast<QObject*>(obj),
QMetaObject::RegisterMethodArgumentMetaType, QMetaObject::RegisterMethodArgumentMetaType,
member.methodIndex(), argv); member.methodIndex(), argv);
locker.relock();
} }
if (!tp.isValid()) { if (!tp.isValid()) {
qWarning("QSignalSpy: Unable to handle parameter '%s' of type '%s' of method '%s'," qWarning("QSignalSpy: Unable to handle parameter '%s' of type '%s' of method '%s',"
@ -179,6 +190,7 @@ private:
void appendArgs(void **a) void appendArgs(void **a)
{ {
QMutexLocker locker(&m_mutex);
QList<QVariant> list; QList<QVariant> list;
list.reserve(args.size()); list.reserve(args.size());
for (int i = 0; i < args.size(); ++i) { for (int i = 0; i < args.size(); ++i) {
@ -190,9 +202,11 @@ private:
} }
append(list); append(list);
if (m_waiting) if (m_waiting) {
locker.unlock();
m_loop.exitLoop(); m_loop.exitLoop();
} }
}
// the full, normalized signal name // the full, normalized signal name
QByteArray sig; QByteArray sig;
@ -201,6 +215,7 @@ private:
QTestEventLoop m_loop; QTestEventLoop m_loop;
bool m_waiting; bool m_waiting;
static inline QMutex m_mutex; // protects m_waiting, args and the QList base class, between appendArgs() and wait()
}; };
QT_END_NAMESPACE QT_END_NAMESPACE

View File

@ -6,9 +6,11 @@
#include <QSignalSpy> #include <QSignalSpy>
#include <QTimer> #include <QTimer>
#include <qdatetime.h> #include <qdatetime.h>
using namespace std::chrono_literals;
using namespace Qt::StringLiterals;
class tst_QSignalSpy : public QObject class tst_QSignalSpy : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -48,6 +50,8 @@ private slots:
void spyOnMetaMethod_invalid(); void spyOnMetaMethod_invalid();
void spyOnMetaMethod_invalid_data(); void spyOnMetaMethod_invalid_data();
void signalSpyDoesNotRaceOnCrossThreadSignal();
}; };
struct CustomType {}; struct CustomType {};
@ -485,5 +489,31 @@ void tst_QSignalSpy::spyOnMetaMethod_invalid_data()
<< QObject::staticMetaObject.method(QObject::staticMetaObject.indexOfMethod("deleteLater()")); << QObject::staticMetaObject.method(QObject::staticMetaObject.indexOfMethod("deleteLater()"));
} }
class EmitSignal_Thread : public QThread
{
Q_OBJECT
public:
void run() override
{
emit valueChanged(42, u"is the answer"_s);
}
Q_SIGNALS:
void valueChanged(int value, const QString &str);
};
void tst_QSignalSpy::signalSpyDoesNotRaceOnCrossThreadSignal()
{
EmitSignal_Thread thread;
QSignalSpy valueChangedSpy(&thread, &EmitSignal_Thread::valueChanged);
QVERIFY(valueChangedSpy.isValid());
thread.start();
QVERIFY(valueChangedSpy.wait(5s));
QCOMPARE(valueChangedSpy[0][0].toInt(), 42);
QCOMPARE(valueChangedSpy[0][1].toString(), u"is the answer"_s);
QVERIFY(thread.wait(5s));
}
QTEST_MAIN(tst_QSignalSpy) QTEST_MAIN(tst_QSignalSpy)
#include "tst_qsignalspy.moc" #include "tst_qsignalspy.moc"