QThread/Unix: make QThreadPrivate::finish() be called much later

We need it to run after all the thread-local destructors have run, to
ensure that some user code hasn't run after QThreadPrivate::finish() has
finished. We achieve that by making it get called from a thread-local
destructor itself, in the form of a qScopeGuard.

This ought to have been done since C++11 thread_local with non-trivial
destructors became available. However, it only started showing up after
commit 4a93285b166ceceaea2e10c8fc6a254d2f7093b9 began using thread_local
inside Qt itself. The visible symptom was that QThreadPrivate::finish()
had already destroyed the thread's event dispatcher, but some user code
ran later and expected it to still exist (or, worse, recreated it, via
QEventLoop → QThreadData::ensureEventDispatcher).

Fixes: QTBUG-117996
Pick-to: 6.7
Change-Id: I8f3ce163ccc5408cac39fffd178d682e5bfa6955
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Thiago Macieira 2023-10-12 09:13:02 -07:00
parent 3fc5ee5c2e
commit 1ed0dd88a3
2 changed files with 66 additions and 22 deletions

View File

@ -282,8 +282,12 @@ void *QThreadPrivate::start(void *arg)
#ifdef PTHREAD_CANCEL_DISABLE #ifdef PTHREAD_CANCEL_DISABLE
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr); pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr);
#endif #endif
pthread_cleanup_push(QThreadPrivate::finish, arg); #if !defined(Q_OS_QNX)
// On QNX, calling finish() from a thread_local destructor causes the C
// library to hang.
static thread_local
#endif
auto cleanup = qScopeGuard([=] { finish(arg); });
terminate_on_exception([&] { terminate_on_exception([&] {
QThread *thr = reinterpret_cast<QThread *>(arg); QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr); QThreadData *data = QThreadData::get2(thr);
@ -328,11 +332,7 @@ void *QThreadPrivate::start(void *arg)
thr->run(); thr->run();
}); });
// This pop runs finish() below. It's outside the try/catch (and has its // The qScopeGuard above call runs finish() below.
// own try/catch) to prevent finish() to be run in case an exception is
// thrown.
pthread_cleanup_pop(1);
return nullptr; return nullptr;
} }

View File

@ -1299,8 +1299,16 @@ public:
} }
void registerSocketNotifier(QSocketNotifier *) override {} void registerSocketNotifier(QSocketNotifier *) override {}
void unregisterSocketNotifier(QSocketNotifier *) override {} void unregisterSocketNotifier(QSocketNotifier *) override {}
void registerTimer(Qt::TimerId, Duration, Qt::TimerType, QObject *) override {} void registerTimer(Qt::TimerId id, Duration, Qt::TimerType, QObject *) override
bool unregisterTimer(Qt::TimerId) override { return false; } {
if (registeredTimerId <= Qt::TimerId::Invalid)
registeredTimerId = id;
}
bool unregisterTimer(Qt::TimerId id) override
{
Qt::TimerId oldId = std::exchange(registeredTimerId, Qt::TimerId::Invalid);
return id == oldId;
}
bool unregisterTimers(QObject *) override { return false; } bool unregisterTimers(QObject *) override { return false; }
QList<TimerInfoV2> timersForObject(QObject *) const override { return {}; } QList<TimerInfoV2> timersForObject(QObject *) const override { return {}; }
Duration remainingTime(Qt::TimerId) const override { return 0s; } Duration remainingTime(Qt::TimerId) const override { return 0s; }
@ -1313,25 +1321,47 @@ public:
#endif #endif
QBasicAtomicInt visited; // bool QBasicAtomicInt visited; // bool
Qt::TimerId registeredTimerId = Qt::TimerId::Invalid;
}; };
class ThreadObj : public QObject struct ThreadLocalContent
{ {
Q_OBJECT static inline const QMetaObject *atStart;
public slots: static inline const QMetaObject *atEnd;
void visit() { QSemaphore *sem;
emit visited(); QBasicTimer timer;
ThreadLocalContent(QObject *obj, QSemaphore *sem)
: sem(sem)
{
ensureEventDispatcher();
atStart = QAbstractEventDispatcher::instance()->metaObject();
timer.start(10s, obj);
}
~ThreadLocalContent()
{
ensureEventDispatcher();
atEnd = QAbstractEventDispatcher::instance()->metaObject();
timer.stop();
sem->release();
}
void ensureEventDispatcher()
{
// QEventLoop's constructor has a call to QThreadData::ensureEventDispatcher()
QEventLoop dummy;
} }
signals:
void visited();
}; };
void tst_QThread::customEventDispatcher() void tst_QThread::customEventDispatcher()
{ {
ThreadLocalContent::atStart = ThreadLocalContent::atEnd = nullptr;
QThread thr; QThread thr;
// there should be no ED yet // there should be no ED yet
QVERIFY(!thr.eventDispatcher()); QVERIFY(!thr.eventDispatcher());
DummyEventDispatcher *ed = new DummyEventDispatcher; DummyEventDispatcher *ed = new DummyEventDispatcher;
QPointer<DummyEventDispatcher> weak_ed(ed);
thr.setEventDispatcher(ed); thr.setEventDispatcher(ed);
// the new ED should be set // the new ED should be set
QCOMPARE(thr.eventDispatcher(), ed); QCOMPARE(thr.eventDispatcher(), ed);
@ -1340,25 +1370,39 @@ void tst_QThread::customEventDispatcher()
thr.start(); thr.start();
// start() should not overwrite the ED // start() should not overwrite the ED
QCOMPARE(thr.eventDispatcher(), ed); QCOMPARE(thr.eventDispatcher(), ed);
QVERIFY(!weak_ed.isNull());
ThreadObj obj; QObject obj;
obj.moveToThread(&thr); obj.moveToThread(&thr);
// move was successful? // move was successful?
QCOMPARE(obj.thread(), &thr); QCOMPARE(obj.thread(), &thr);
QEventLoop loop;
connect(&obj, SIGNAL(visited()), &loop, SLOT(quit()), Qt::QueuedConnection); QSemaphore threadLocalSemaphore;
QMetaObject::invokeMethod(&obj, "visit", Qt::QueuedConnection); QMetaObject::invokeMethod(&obj, [&]() {
loop.exec(); #ifndef Q_OS_WIN
// On Windows, the thread_locals are unsequenced between DLLs, so this
// could run after QThreadPrivate::finish()
static thread_local
#endif
ThreadLocalContent d(&obj, &threadLocalSemaphore);
}, Qt::BlockingQueuedConnection);
// test that the ED has really been used // test that the ED has really been used
QVERIFY(ed->visited.loadRelaxed()); QVERIFY(ed->visited.loadRelaxed());
// and it's ours
QCOMPARE(ThreadLocalContent::atStart->className(), "DummyEventDispatcher");
QPointer<DummyEventDispatcher> weak_ed(ed);
QVERIFY(!weak_ed.isNull()); QVERIFY(!weak_ed.isNull());
thr.quit(); thr.quit();
// wait for thread to be stopped // wait for thread to be stopped
QVERIFY(thr.wait(30000)); QVERIFY(thr.wait(30000));
QVERIFY(threadLocalSemaphore.tryAcquire(1, 30s));
// test that ED has been deleted // test that ED has been deleted
QVERIFY(weak_ed.isNull()); QVERIFY(weak_ed.isNull());
// test that ED was ours
QCOMPARE(ThreadLocalContent::atEnd->className(), "DummyEventDispatcher");
} }
class Job : public QObject class Job : public QObject