qt5base-lts/tests/auto/other/qobjectrace/tst_qobjectrace.cpp
Lars Knoll 71b4d4f150 Fix crash in concurrent disconnect
This does not fix all data races that we have in the system yet.
One major issue is the virtual disconnectNotify(), that can be
called from any thread and thus is inherently problematic, as it
can collide with the object getting destroyed at the same time
in another thread.

Pick-to: 6.2 6.1 5.15
Task-number: QTBUG-88248
Change-Id: I9d841eb363b7e4f0de1657aeb8f5340d0fd55190
Reviewed-by: Andrei Golubev <andrei.golubev@qt.io>
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
2021-06-17 08:56:22 +02:00

616 lines
18 KiB
C++

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtCore>
#include <QTest>
#include <QtTest/private/qemulationdetector_p.h>
enum { OneMinute = 60 * 1000,
TwoMinutes = OneMinute * 2 };
struct Functor
{
void operator()() const {};
};
class tst_QObjectRace: public QObject
{
Q_OBJECT
private slots:
void moveToThreadRace();
void destroyRace();
void blockingQueuedDestroyRace();
void disconnectRace();
void disconnectRace2();
};
class RaceObject : public QObject
{
Q_OBJECT
QList<QThread *> threads;
int count;
public:
RaceObject()
: count(0)
{ }
void addThread(QThread *thread)
{ threads.append(thread); }
public slots:
void theSlot()
{
enum { step = 35 };
if ((++count % step) == 0) {
QThread *nextThread = threads.at((count / step) % threads.size());
moveToThread(nextThread);
}
}
void destroSlot() {
emit theSignal();
}
signals:
void theSignal();
};
class RaceThread : public QThread
{
Q_OBJECT
RaceObject *object;
QElapsedTimer stopWatch;
public:
RaceThread()
: object(0)
{ }
void setObject(RaceObject *o)
{
object = o;
object->addThread(this);
}
void start() {
stopWatch.start();
QThread::start();
}
void run() override
{
QTimer zeroTimer;
connect(&zeroTimer, SIGNAL(timeout()), object, SLOT(theSlot()));
connect(&zeroTimer, SIGNAL(timeout()), this, SLOT(checkStopWatch()), Qt::DirectConnection);
zeroTimer.start(0);
(void) exec();
}
signals:
void theSignal();
private slots:
void checkStopWatch()
{
if (stopWatch.elapsed() >= 5000)
quit();
QObject o;
connect(&o, SIGNAL(destroyed()) , object, SLOT(destroSlot()));
connect(object, SIGNAL(destroyed()) , &o, SLOT(deleteLater()));
}
};
void tst_QObjectRace::moveToThreadRace()
{
RaceObject *object = new RaceObject;
enum { ThreadCount = 6 };
RaceThread *threads[ThreadCount];
for (int i = 0; i < ThreadCount; ++i) {
threads[i] = new RaceThread;
threads[i]->setObject(object);
}
object->moveToThread(threads[0]);
for (int i = 0; i < ThreadCount; ++i)
threads[i]->start();
while(!threads[0]->isFinished()) {
QPointer<RaceObject> foo (object);
QObject o;
connect(&o, SIGNAL(destroyed()) , object, SLOT(destroSlot()));
connect(object, SIGNAL(destroyed()) , &o, SLOT(deleteLater()));
QTest::qWait(10);
}
// the other threads should finish pretty quickly now
for (int i = 1; i < ThreadCount; ++i)
QVERIFY(threads[i]->wait(300));
for (int i = 0; i < ThreadCount; ++i)
delete threads[i];
delete object;
}
class MyObject : public QObject
{ Q_OBJECT
bool ok;
public:
MyObject() : ok(true) {}
~MyObject() { Q_ASSERT(ok); ok = false; }
public slots:
void slot1() { Q_ASSERT(ok); }
void slot2() { Q_ASSERT(ok); }
void slot3() { Q_ASSERT(ok); }
void slot4() { Q_ASSERT(ok); }
void slot5() { Q_ASSERT(ok); }
void slot6() { Q_ASSERT(ok); }
void slot7() { Q_ASSERT(ok); }
signals:
void signal1();
void signal2();
void signal3();
void signal4();
void signal5();
void signal6();
void signal7();
};
namespace {
const char *_slots[] = { SLOT(slot1()) , SLOT(slot2()) , SLOT(slot3()),
SLOT(slot4()) , SLOT(slot5()) , SLOT(slot6()),
SLOT(slot7()) };
const char *_signals[] = { SIGNAL(signal1()), SIGNAL(signal2()), SIGNAL(signal3()),
SIGNAL(signal4()), SIGNAL(signal5()), SIGNAL(signal6()),
SIGNAL(signal7()) };
typedef void (MyObject::*PMFType)();
const PMFType _slotsPMF[] = { &MyObject::slot1, &MyObject::slot2, &MyObject::slot3,
&MyObject::slot4, &MyObject::slot5, &MyObject::slot6,
&MyObject::slot7 };
const PMFType _signalsPMF[] = { &MyObject::signal1, &MyObject::signal2, &MyObject::signal3,
&MyObject::signal4, &MyObject::signal5, &MyObject::signal6,
&MyObject::signal7 };
}
class DestroyThread : public QThread
{
Q_OBJECT
MyObject **objects;
int number;
public:
void setObjects(MyObject **o, int n)
{
objects = o;
number = n;
for(int i = 0; i < number; i++)
objects[i]->moveToThread(this);
}
void run() override
{
for (int i = number-1; i >= 0; --i) {
/* Do some more connection and disconnection between object in this thread that have not been destroyed yet */
const int nAlive = i+1;
connect (objects[((i+1)*31) % nAlive], _signals[(12*i)%7], objects[((i+2)*37) % nAlive], _slots[(15*i+2)%7] );
disconnect(objects[((i+1)*31) % nAlive], _signals[(12*i)%7], objects[((i+2)*37) % nAlive], _slots[(15*i+2)%7] );
connect (objects[((i+4)*41) % nAlive], _signalsPMF[(18*i)%7], objects[((i+5)*43) % nAlive], _slotsPMF[(19*i+2)%7] );
disconnect(objects[((i+4)*41) % nAlive], _signalsPMF[(18*i)%7], objects[((i+5)*43) % nAlive], _slotsPMF[(19*i+2)%7] );
QMetaObject::Connection c = connect(objects[((i+5)*43) % nAlive], _signalsPMF[(9*i+1)%7], Functor());
for (int f = 0; f < 7; ++f)
emit (objects[i]->*_signalsPMF[f])();
disconnect(c);
disconnect(objects[i], _signalsPMF[(10*i+5)%7], 0, 0);
disconnect(objects[i], _signals[(11*i+6)%7], 0, 0);
disconnect(objects[i], 0, objects[(i*17+6) % nAlive], 0);
if (i%4 == 1) {
disconnect(objects[i], 0, 0, 0);
}
delete objects[i];
}
//run the possible queued slots
qApp->processEvents();
}
};
#define EXTRA_THREAD_WAIT 3000
#define MAIN_THREAD_WAIT TwoMinutes
void tst_QObjectRace::destroyRace()
{
if (QTestPrivate::isRunningArmOnX86())
QSKIP("Test is too slow to run on emulator");
enum { ThreadCount = 10, ObjectCountPerThread = 2777,
ObjectCount = ThreadCount * ObjectCountPerThread };
MyObject *objects[ObjectCount];
for (int i = 0; i < ObjectCount; ++i)
objects[i] = new MyObject;
for (int i = 0; i < ObjectCount * 17; ++i) {
connect(objects[(i*13) % ObjectCount], _signals[(2*i)%7],
objects[((i+2)*17) % ObjectCount], _slots[(3*i+2)%7] );
connect(objects[((i+6)*23) % ObjectCount], _signals[(5*i+4)%7],
objects[((i+8)*41) % ObjectCount], _slots[(i+6)%7] );
connect(objects[(i*67) % ObjectCount], _signalsPMF[(2*i)%7],
objects[((i+1)*71) % ObjectCount], _slotsPMF[(3*i+2)%7] );
connect(objects[((i+3)*73) % ObjectCount], _signalsPMF[(5*i+4)%7],
objects[((i+5)*79) % ObjectCount], Functor() );
}
DestroyThread *threads[ThreadCount];
for (int i = 0; i < ThreadCount; ++i) {
threads[i] = new DestroyThread;
threads[i]->setObjects(objects + i*ObjectCountPerThread, ObjectCountPerThread);
}
for (int i = 0; i < ThreadCount; ++i)
threads[i]->start();
QVERIFY(threads[0]->wait(MAIN_THREAD_WAIT));
// the other threads should finish pretty quickly now
for (int i = 1; i < ThreadCount; ++i)
QVERIFY(threads[i]->wait(EXTRA_THREAD_WAIT));
for (int i = 0; i < ThreadCount; ++i)
delete threads[i];
}
class BlockingQueuedDestroyRaceObject : public QObject
{
Q_OBJECT
public:
enum class Behavior { Normal, Crash };
explicit BlockingQueuedDestroyRaceObject(Behavior b = Behavior::Normal)
: m_behavior(b) {}
signals:
bool aSignal();
public slots:
bool aSlot()
{
switch (m_behavior) {
case Behavior::Normal:
return true;
case Behavior::Crash:
qFatal("Race detected in a blocking queued connection");
break;
}
Q_UNREACHABLE();
return false;
}
private:
Behavior m_behavior;
};
void tst_QObjectRace::blockingQueuedDestroyRace()
{
#if !QT_CONFIG(cxx11_future)
QSKIP("This test requires QThread::create");
#else
enum { MinIterations = 100, MinTime = 3000, WaitTime = 25 };
BlockingQueuedDestroyRaceObject sender;
QDeadlineTimer timer(MinTime);
int iteration = 0;
while (iteration++ < MinIterations || !timer.hasExpired()) {
// Manually allocate some storage, and create a receiver in there
std::aligned_storage<
sizeof(BlockingQueuedDestroyRaceObject),
alignof(BlockingQueuedDestroyRaceObject)
>::type storage;
auto *receiver = reinterpret_cast<BlockingQueuedDestroyRaceObject *>(&storage);
new (receiver) BlockingQueuedDestroyRaceObject(BlockingQueuedDestroyRaceObject::Behavior::Normal);
// Connect it to the sender via BlockingQueuedConnection
QVERIFY(connect(&sender, &BlockingQueuedDestroyRaceObject::aSignal,
receiver, &BlockingQueuedDestroyRaceObject::aSlot,
Qt::BlockingQueuedConnection));
const auto emitUntilDestroyed = [&sender] {
// Hack: as long as the receiver is alive and the connection
// established, the signal will return true (from the slot).
// When the receiver gets destroyed, the signal is disconnected
// and therefore the emission returns false.
while (emit sender.aSignal())
;
};
std::unique_ptr<QThread> thread(QThread::create(emitUntilDestroyed));
thread->start();
QTest::qWait(WaitTime);
// Destroy the receiver, and immediately allocate a new one at
// the same address. In case of a race, this might cause:
// - the metacall event to be posted to a destroyed object;
// - the metacall event to be posted to the wrong object.
// In both cases we hope to catch the race by crashing.
receiver->~BlockingQueuedDestroyRaceObject();
new (receiver) BlockingQueuedDestroyRaceObject(BlockingQueuedDestroyRaceObject::Behavior::Crash);
// Flush events
QTest::qWait(0);
thread->wait();
receiver->~BlockingQueuedDestroyRaceObject();
}
#endif
}
static QAtomicInteger<unsigned> countedStructObjectsCount;
struct CountedFunctor
{
CountedFunctor() : destroyed(false) { countedStructObjectsCount.fetchAndAddRelaxed(1); }
CountedFunctor(const CountedFunctor &) : destroyed(false) { countedStructObjectsCount.fetchAndAddRelaxed(1); }
CountedFunctor &operator=(const CountedFunctor &) { return *this; }
~CountedFunctor() { destroyed = true; countedStructObjectsCount.fetchAndAddRelaxed(-1);}
void operator()() const {QCOMPARE(destroyed, false);}
private:
bool destroyed;
};
class DisconnectRaceSenderObject : public QObject
{
Q_OBJECT
signals:
void theSignal();
};
class DisconnectRaceThread : public QThread
{
Q_OBJECT
DisconnectRaceSenderObject *sender;
bool emitSignal;
public:
DisconnectRaceThread(DisconnectRaceSenderObject *s, bool emitIt)
: QThread(), sender(s), emitSignal(emitIt)
{
}
void run() override
{
while (!isInterruptionRequested()) {
QMetaObject::Connection conn = connect(sender, &DisconnectRaceSenderObject::theSignal,
sender, CountedFunctor(), Qt::BlockingQueuedConnection);
if (emitSignal)
emit sender->theSignal();
disconnect(conn);
yieldCurrentThread();
}
}
};
class DeleteReceiverRaceSenderThread : public QThread
{
Q_OBJECT
DisconnectRaceSenderObject *sender;
public:
DeleteReceiverRaceSenderThread(DisconnectRaceSenderObject *s)
: QThread(), sender(s)
{
}
void run() override
{
while (!isInterruptionRequested()) {
emit sender->theSignal();
yieldCurrentThread();
}
}
};
class DeleteReceiverRaceReceiver : public QObject
{
Q_OBJECT
DisconnectRaceSenderObject *sender;
QObject *receiver;
QTimer *timer;
public:
DeleteReceiverRaceReceiver(DisconnectRaceSenderObject *s)
: QObject(), sender(s), receiver(0)
{
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &DeleteReceiverRaceReceiver::onTimeout);
timer->start(1);
}
~DeleteReceiverRaceReceiver()
{
delete receiver;
}
void onTimeout()
{
if (receiver)
delete receiver;
receiver = new QObject;
connect(sender, &DisconnectRaceSenderObject::theSignal, receiver, CountedFunctor(), Qt::BlockingQueuedConnection);
}
};
class DeleteReceiverRaceReceiverThread : public QThread
{
Q_OBJECT
DisconnectRaceSenderObject *sender;
public:
DeleteReceiverRaceReceiverThread(DisconnectRaceSenderObject *s)
: QThread(), sender(s)
{
}
void run() override
{
QScopedPointer<DeleteReceiverRaceReceiver> receiver(new DeleteReceiverRaceReceiver(sender));
exec();
}
};
void tst_QObjectRace::disconnectRace()
{
enum { ThreadCount = 20, TimeLimit = 3000 };
QCOMPARE(countedStructObjectsCount.loadRelaxed(), 0u);
{
QScopedPointer<DisconnectRaceSenderObject> sender(new DisconnectRaceSenderObject());
QScopedPointer<QThread> senderThread(new QThread());
senderThread->start();
sender->moveToThread(senderThread.data());
DisconnectRaceThread *threads[ThreadCount];
for (int i = 0; i < ThreadCount; ++i) {
threads[i] = new DisconnectRaceThread(sender.data(), !(i % 10));
threads[i]->start();
}
QTest::qWait(TimeLimit);
for (int i = 0; i < ThreadCount; ++i) {
threads[i]->requestInterruption();
QVERIFY(threads[i]->wait());
delete threads[i];
}
senderThread->quit();
QVERIFY(senderThread->wait());
}
QCOMPARE(countedStructObjectsCount.loadRelaxed(), 0u);
{
QScopedPointer<DisconnectRaceSenderObject> sender(new DisconnectRaceSenderObject());
QScopedPointer<DeleteReceiverRaceSenderThread> senderThread(new DeleteReceiverRaceSenderThread(sender.data()));
senderThread->start();
sender->moveToThread(senderThread.data());
DeleteReceiverRaceReceiverThread *threads[ThreadCount];
for (int i = 0; i < ThreadCount; ++i) {
threads[i] = new DeleteReceiverRaceReceiverThread(sender.data());
threads[i]->start();
}
QTest::qWait(TimeLimit);
senderThread->requestInterruption();
QVERIFY(senderThread->wait());
for (int i = 0; i < ThreadCount; ++i) {
threads[i]->quit();
QVERIFY(threads[i]->wait());
delete threads[i];
}
}
QCOMPARE(countedStructObjectsCount.loadRelaxed(), 0u);
}
void tst_QObjectRace::disconnectRace2()
{
enum { IterationCount = 100, ConnectionCount = 100, YieldCount = 100 };
QAtomicPointer<MyObject> ptr;
QSemaphore createSemaphore(0);
QSemaphore proceedSemaphore(0);
std::unique_ptr<QThread> t1(QThread::create([&]() {
for (int i = 0; i < IterationCount; ++i) {
MyObject sender;
ptr.storeRelease(&sender);
createSemaphore.release();
proceedSemaphore.acquire();
ptr.storeRelaxed(nullptr);
for (int i = 0; i < YieldCount; ++i)
QThread::yieldCurrentThread();
}
}));
t1->start();
std::unique_ptr<QThread> t2(QThread::create([&]() {
auto connections = std::make_unique<QMetaObject::Connection[]>(ConnectionCount);
for (int i = 0; i < IterationCount; ++i) {
MyObject receiver;
MyObject *sender = nullptr;
createSemaphore.acquire();
while (!(sender = ptr.loadAcquire()))
;
for (int i = 0; i < ConnectionCount; ++i)
connections[i] = QObject::connect(sender, &MyObject::signal1, &receiver, &MyObject::slot1);
proceedSemaphore.release();
for (int i = 0; i < ConnectionCount; ++i)
QObject::disconnect(connections[i]);
}
}));
t2->start();
t1->wait();
t2->wait();
}
QTEST_MAIN(tst_QObjectRace)
#include "tst_qobjectrace.moc"