Потоки и многопоточность

Сигналы и слоты между потоками: Qt::QueuedConnection на практике

1 просмотров

Введение

Одна из сильных сторон сигнально-слотовой системы 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().