Как thread affinity влияет на signals/slots и QObject ownership?
Каждый QObject имеет affinity — поток, в котором он был создан. Прямое соединение выполняется синхронно в потоке отправителя; queued-соединение ставит событие в очередь потока получателя и позволяет безопасно общаться между потоками.
Thread Affinity в Qt
Каждый QObject принадлежит ровно одному потоку — тому, в котором он был сконструирован. Это называется thread affinity. Узнать текущий поток объекта можно через obj.thread(), а сменить — через obj.moveToThread(&otherThread).
Thread affinity определяет:
- В каком потоке будут обрабатываться события объекта (
QEvent). - Какой тип соединения выбирает Qt при вызове слота через
emit. - Кто имеет право удалять объект без UB.
Типы соединений сигнал–слот
Qt::DirectConnection— слот вызывается немедленно в потоке, где вызванemit, независимо от affinity получателя. Опасно из другого потока.Qt::QueuedConnection— создаётсяQMetaCallEventи помещается в очередь событий потока получателя. Слот выполняется в правильном потоке.Qt::BlockingQueuedConnection— как Queued, но поток отправителя блокируется до завершения слота. Дедлок, если оба объекта в одном потоке.Qt::AutoConnection(умолчание) — Qt выбирает Direct, если отправитель и получатель в одном потоке, иначе Queued.
Практический пример: Worker в фоновом потоке
#include <QObject>
#include <QThread>
#include <QDebug>
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork(const QString ¶m) {
qDebug() << "Working in" << QThread::currentThread();
emit resultReady(param.toUpper());
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject {
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker; // affinity = main thread
worker->moveToThread(&workerThread); // affinity = workerThread
connect(&workerThread, &QThread::finished,
worker, &QObject::deleteLater); // безопасное удаление
connect(this, &Controller::operate,
worker, &Worker::doWork); // AutoConnection → Queued
connect(worker, &Worker::resultReady,
this, &Controller::handleResult); // AutoConnection → Queued
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
signals:
void operate(const QString ¶m);
public slots:
void handleResult(const QString &result) {
qDebug() << "Result:" << result;
}
};
Ключевой момент: worker создаётся в главном потоке, затем переносится через moveToThread. После переноса его слоты будут выполняться в workerThread через Queued-соединение.
Правила владения (Ownership)
- Parent ownership: объект с родителем удаляется, когда удаляется родитель. Родитель и дочерний объект должны быть в одном потоке.
- moveToThread и parent: нельзя перенести объект в другой поток, если у него есть parent.
moveToThreadвернёт предупреждение. - Удаление из другого потока:
delete workerиз чужого потока — UB. Используйтеworker->deleteLater(): это безопасно из любого потока, объект будет удалён в своём потоке при следующей итерации event loop.
Передача аргументов через QueuedConnection
Типы аргументов сигнала должны быть зарегистрированы в мета-системе, если они не встроены:
// Регистрация пользовательского типа
qRegisterMetaType<MyStruct>("MyStruct");
// Или через Q_DECLARE_METATYPE + qRegisterMetaType в main()
Без регистрации Qt тихо проглотит сигнал и выведет предупреждение в runtime: «Cannot queue arguments of type 'MyStruct'».
Подводные камни
- Создание QObject в чужом потоке: если создать объект внутри
QThread::run()без event loop (exec()), Queued-сигналы не доставятся — очередь событий пуста. - moveToThread с parent: попытка перенести объект, у которого есть QObject-родитель, завершается предупреждением и отказом; всегда сначала открепляйте от родителя.
- BlockingQueuedConnection дедлок: если отправитель и получатель в одном потоке, поток ждёт сам себя — приложение зависает.
- DirectConnection из чужого потока: явно указав
Qt::DirectConnectionдля объекта в другом потоке, вы получаете data race без предупреждений. - deleteLater в потоке без event loop: если поток завершился до обработки отложенного удаления, объект не будет удалён — утечка памяти. Завершайте поток через
quit() + wait()после того, как всеdeleteLaterобработаны. - Незарегистрированный тип в Queued: данные не передадутся, сигнал «потеряется» без явной ошибки компилятора.
- QTimer и affinity:
QTimerсрабатывает в потоке, которому принадлежит; если он перенесён черезmoveToThread, таймер нужно перезапустить после переноса.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.