Введение
Одна из сильных сторон сигнально-слотовой системы Qt — она прозрачно работает между потоками, если объекты-получатели «живут» (имеют affinity) в разных потоках. Это решает классическую проблему многопоточного программирования: как безопасно передать данные из рабочего потока в основной без явных мьютексов и блокировок. В статье разберём механику Qt::QueuedConnection и типичные ошибки при межпотоковом взаимодействии.
Концепция
Каждый QObject принадлежит (affinity) тому потоку, в котором он был создан или в который был перемещён через moveToThread(). Когда сигнал из одного потока подключён к слоту объекта, принадлежащего другому потоку, Qt автоматически использует Qt::AutoConnection, который превращается в Qt::QueuedConnection: вызов слота не происходит немедленно, а упаковывается в событие и помещается в очередь событий потока-получателя, где будет обработан в следующей итерации его event loop.
Это значит, что фактическое выполнение слота откладывается, а аргументы сигнала копируются (а не передаются по ссылке) — поэтому в межпотоковых сигналах нельзя передавать указатели на стековые объекты, которые могут быть разрушены раньше, чем слот выполнится. Для объектов, не зарегистрированных через qRegisterMetaType(), передача пользовательских типов в очередь невозможна — Qt должен знать, как их копировать.
Пример кода
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
class Worker : public QObject
{
Q_OBJECT
public:
void process()
{
for (int i = 0; i <= 100; i += 25) {
QThread::msleep(300);
emit progressChanged(i); // выполняется в потоке Worker
}
emit finished();
}
signals:
void progressChanged(int percent);
void finished();
};
class MainController : public QObject
{
Q_OBJECT
public:
void run()
{
auto *thread = new QThread(this);
auto *worker = new Worker;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::process);
connect(worker, &Worker::progressChanged, this, &MainController::onProgress);
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(worker, &Worker::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
connect(thread, &QThread::finished, qApp, &QCoreApplication::quit);
thread->start();
}
private slots:
void onProgress(int percent)
{
// Этот слот выполняется в основном потоке через QueuedConnection
qDebug() << "Прогресс:" << percent << "% (поток:" << QThread::currentThreadId() << ")";
}
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
MainController controller;
controller.run();
return app.exec();
}
Пояснения к коду
Worker создаётся в основном потоке, но сразу перемещается в новый поток через moveToThread(thread) — после этого все его слоты будут выполняться в потоке thread, а не в том потоке, откуда был вызван moveToThread. Сигнал progressChanged, испускаемый внутри process() (выполняется в рабочем потоке), подключён к слоту onProgress объекта MainController, который живёт в основном потоке — Qt автоматически определяет, что подключение межпотоковое, и доставляет вызов асинхронно через очередь событий.
Цепочка finished -> quit -> deleteLater -> finished -> deleteLater — стандартный идиоматичный паттерн корректного завершения и очистки рабочего потока в Qt, избегающий утечек памяти.
Подводные камни
- Передача указателей на временные объекты через сигнал. Если сигнал передаёт
QObject*на объект, который может быть удалён до того, как очередь событий доставит вызов слота, в обработчике произойдёт обращение к освобождённой памяти. Передавайте значения или умные указатели, либо гарантируйте время жизни объекта. - Незарегистрированные пользовательские типы. Для передачи структур или классов, не являющихся встроенными типами Qt, через межпотоковый сигнал нужно зарегистрировать их через
qRegisterMetaType<MyType>("MyType"), иначе Qt не сможет скопировать аргумент для очереди и выдаст предупреждение во время выполнения. - Прямой вызов метода объекта из другого потока вместо сигнала. Вызов
worker->someMethod()напрямую из основного потока выполнится синхронно в контексте вызывающего потока (а не потокаworker), что не даёт никакой потокобезопасности — для безопасного асинхронного вызова нужен именно сигнал/слот илиQMetaObject::invokeMethod()с явным указаниемQt::QueuedConnection. - Подключение сигналов до перемещения в поток vs после. Порядок
connect()относительноmoveToThread()не имеет значения для типа соединения — Qt определяет его динамически в момент эмиссии сигнала на основе текущей принадлежности объектов потокам, а не в моментconnect().