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