277c23956c
The current [basic.life] wording seems to cover the existing code, but IIRC, older versions of [basic.life] were not so relaxed. In particular, while not completely pertinent, the second placement new is awfully similar to http://eel.is/c++draft/ptr.launder#example-1 Just make all of this SEP and use std::optional. That way, the code gets simpler, too, plus we get rid of the last use of C++23-deprecated std::aligned_storage. The reset() before the 2nd emplace() isn't necessary, but, in a test, it doesn't hurt, either, and keeps code readers from guessing whether the first-emplaced object's dtor is actually properly run (it is). Pick-to: 6.3 6.2 Fixes: QTBUG-99122 Change-Id: If31a46f8be3a74499f1176133029d097faf7dfe9 Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
612 lines
17 KiB
C++
612 lines
17 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>
|
|
|
|
#include <optional>
|
|
|
|
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::optional<BlockingQueuedDestroyRaceObject> receiver;
|
|
|
|
receiver.emplace(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.reset();
|
|
receiver.emplace(BlockingQueuedDestroyRaceObject::Behavior::Crash);
|
|
|
|
// Flush events
|
|
QTest::qWait(0);
|
|
|
|
thread->wait();
|
|
}
|
|
#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"
|