Различные статьи по Qt и c++

QThread для начинающих: первый поток с нуля

Введение

Если ваше приложение выполняет долгую операцию (загрузка большого файла, сложные вычисления, ожидание ответа от внешнего устройства) прямо в основном потоке, интерфейс «зависает» — окно не перерисовывается, кнопки не реагируют на клики. Решение — выполнить эту операцию в отдельном потоке, оставив основной поток свободным для обработки интерфейса. В статье разберём самый простой, базовый способ создать и использовать QThread.

Концепция

Самый понятный для начинающих способ работы с QThread — субклассирование: создаётся собственный класс, наследующий QThread, в котором переопределяется метод run() — именно код внутри run() будет выполняться в отдельном, новом потоке. Чтобы запустить поток, создаётся объект этого класса и вызывается его метод start() (не run() напрямую — run() вызывается автоматически внутри нового потока после start()). Чтобы сообщить из потока в основной поток о результате (например, обновить интерфейс), используются сигналы — они автоматически и безопасно доставляются между потоками.

Пример кода: QThread для начинающих

#include <QThread>
#include <QApplication>
#include <QPushButton>
#include <QProgressBar>
#include <QVBoxLayout>
#include <QDebug>

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    void run() override
    {
        // Этот код выполняется в ОТДЕЛЬНОМ потоке, не блокируя интерфейс
        for (int i = 0; i <= 100; i += 10) {
            QThread::msleep(300); // имитация долгой работы
            emit progressUpdated(i);
        }
        emit finished();
    }

signals:
    void progressUpdated(int percent);
    void finished();
};

class MainWindow : public QWidget
{
    Q_OBJECT
public:
    MainWindow()
    {
        auto *startButton = new QPushButton("Запустить долгую операцию", this);
        m_progressBar = new QProgressBar(this);

        auto *layout = new QVBoxLayout(this);
        layout->addWidget(startButton);
        layout->addWidget(m_progressBar);

        connect(startButton, &QPushButton::clicked, this, &MainWindow::startWork);
    }

private slots:
    void startWork()
    {
        auto *worker = new WorkerThread();

        // Сигналы из потока безопасно доставляются в основной поток для обновления UI
        connect(worker, &WorkerThread::progressUpdated, m_progressBar, &QProgressBar::setValue);
        connect(worker, &WorkerThread::finished, this, []() {
            qDebug() << "Операция завершена";
        });

        // Автоматическое удаление объекта потока после завершения его работы
        connect(worker, &WorkerThread::finished, worker, &QObject::deleteLater);

        worker->start(); // запускает run() в новом потоке; интерфейс остаётся отзывчивым
    }

private:
    QProgressBar *m_progressBar;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow window;
    window.show();
    return app.exec();
}

#include "main.moc"

Пояснения к коду

WorkerThread::run() содержит цикл с имитацией долгой работы (QThread::msleep(300)) — поскольку этот код выполняется в отдельном потоке, кнопка «Запустить долгую операцию» и весь остальной интерфейс остаются отзывчивыми во время выполнения цикла, в отличие от ситуации, если бы такой же цикл со sleep был написан прямо в обработчике клика кнопки в основном потоке.

Сигналы progressUpdated/finished, испускаемые внутри run() (то есть из рабочего потока), подключены к слотам объектов основного потока (m_progressBar, лямбда) — Qt автоматически определяет, что это межпотоковое соединение, и доставляет вызов через очередь событий, что безопасно для обновления интерфейса. Подключение finished к worker->deleteLater() гарантирует, что объект потока будет корректно удалён после завершения работы, без необходимости вручную отслеживать его время жизни.

Подводные камни

  • Прямой вызов run() вместо start(). Если по ошибке вызвать worker->run() напрямую, код выполнится синхронно в текущем (основном) потоке, как обычный вызов функции — никакого параллелизма не будет, и интерфейс всё равно «зависнет» на время выполнения. Запускать поток нужно строго через start().
  • Обращение к виджетам интерфейса напрямую из кода внутри run(). Виджеты Qt не потокобезопасны — попытка вызвать, например, m_progressBar->setValue() непосредственно внутри run() (а не через сигнал/слот) может привести к непредсказуемому поведению или крашу. Обновление интерфейса из другого потока должно идти только через сигналы.
  • Забытое удаление объекта потока, если не подключить deleteLater() к сигналу завершения — каждый созданный, но не удалённый WorkerThread остаётся в памяти, и при частом повторном запуске операции такие объекты будут накапливаться.
  • Повторный запуск уже завершённого или ещё работающего объекта QThread через start() снова без создания нового экземпляра — это не поддерживается напрямую и может привести к неожиданному поведению; для каждого нового запуска операции стоит создавать новый объект WorkerThread, как показано в примере.