71b4d4f150
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>
616 lines
18 KiB
C++
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"
|