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
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr);
#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([&] {
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
@ -328,11 +332,7 @@ void *QThreadPrivate::start(void *arg)
thr->run();
});
// This pop runs finish() below. It's outside the try/catch (and has its
// own try/catch) to prevent finish() to be run in case an exception is
// thrown.
pthread_cleanup_pop(1);
// The qScopeGuard above call runs finish() below.
return nullptr;
}

View File

@ -1299,8 +1299,16 @@ public:
}
void registerSocketNotifier(QSocketNotifier *) override {}
void unregisterSocketNotifier(QSocketNotifier *) override {}
void registerTimer(Qt::TimerId, Duration, Qt::TimerType, QObject *) override {}
bool unregisterTimer(Qt::TimerId) override { return false; }
void registerTimer(Qt::TimerId id, Duration, Qt::TimerType, QObject *) override
{
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; }
QList<TimerInfoV2> timersForObject(QObject *) const override { return {}; }
Duration remainingTime(Qt::TimerId) const override { return 0s; }
@ -1313,25 +1321,47 @@ public:
#endif
QBasicAtomicInt visited; // bool
Qt::TimerId registeredTimerId = Qt::TimerId::Invalid;
};
class ThreadObj : public QObject
struct ThreadLocalContent
{
Q_OBJECT
public slots:
void visit() {
emit visited();
static inline const QMetaObject *atStart;
static inline const QMetaObject *atEnd;
QSemaphore *sem;
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()
{
ThreadLocalContent::atStart = ThreadLocalContent::atEnd = nullptr;
QThread thr;
// there should be no ED yet
QVERIFY(!thr.eventDispatcher());
DummyEventDispatcher *ed = new DummyEventDispatcher;
QPointer<DummyEventDispatcher> weak_ed(ed);
thr.setEventDispatcher(ed);
// the new ED should be set
QCOMPARE(thr.eventDispatcher(), ed);
@ -1340,25 +1370,39 @@ void tst_QThread::customEventDispatcher()
thr.start();
// start() should not overwrite the ED
QCOMPARE(thr.eventDispatcher(), ed);
QVERIFY(!weak_ed.isNull());
ThreadObj obj;
QObject obj;
obj.moveToThread(&thr);
// move was successful?
QCOMPARE(obj.thread(), &thr);
QEventLoop loop;
connect(&obj, SIGNAL(visited()), &loop, SLOT(quit()), Qt::QueuedConnection);
QMetaObject::invokeMethod(&obj, "visit", Qt::QueuedConnection);
loop.exec();
QSemaphore threadLocalSemaphore;
QMetaObject::invokeMethod(&obj, [&]() {
#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
QVERIFY(ed->visited.loadRelaxed());
// and it's ours
QCOMPARE(ThreadLocalContent::atStart->className(), "DummyEventDispatcher");
QPointer<DummyEventDispatcher> weak_ed(ed);
QVERIFY(!weak_ed.isNull());
thr.quit();
// wait for thread to be stopped
QVERIFY(thr.wait(30000));
QVERIFY(threadLocalSemaphore.tryAcquire(1, 30s));
// test that ED has been deleted
QVERIFY(weak_ed.isNull());
// test that ED was ours
QCOMPARE(ThreadLocalContent::atEnd->className(), "DummyEventDispatcher");
}
class Job : public QObject