12 KiB
QThread - что нужно знать о потоках
Основы
В Qt любые объекты способные работать с сигналами и слотами являются
наследниками класса QObject
.
Каждый QObject
строго привязан к какому-то потоку QThread
который,
собственно, и занимается обслуживанием слотов и прочих событий данного объекта.
Один поток может обслуживать сразу множество QObject
или вообще ни одного,
а вот QObject
всегда имеет родительский поток и он всегда ровно один.
По сути можно считать что каждый QThread
«владеет» каким-то набором QObject
.
Внутри каждого QThread
спрятана очередь сообщений адресованных
к объектам которыми данный QThread
«владеет».
В модели Qt предполагается что если мы хотим чтобы QObject
сделал
какое-либо действие, то мы «посылаем» данному QObject
сообщение QEvent
.
В этом потоково-безопасном вызове Qt находит QThread
которому принадлежит
объект receiver, записывает QEvent
в очередь сообщений этого потока и при
необходимости «будит» этот поток. При этом ожидается что код работающий в
данном QThread
в какой-то момент после этого прочитает сообщение из
очереди и выполнит соответствующее действие. Чтобы это действительно произошло,
код в QThread
должен войти в цикл обработки событий QEventLoop
,
создав соответствующий объект и позвав у него либо метод exec()
,
либо метод processEvents()
. Первый вариант входит в бесконечный цикл
обработки сообщений (до получения QEventLoop
события quit()
),
второй ограничивается тем что обрабатывает сообщения ранее накопившиеся в очереди.
Итак, как мы уже разобрались, каждый объект в Qt «принадлежит» какому-то потоку. При этом встает закономерный вопрос: а какому, собственно говоря, именно? В Qt приняты следующие соглашения:
-
Все «дети» любого «родителя» всегда живут в том же потоке что и родительский объект. Например попытка сделать
setParent
к объекту живущему в другом потоке в Qt просто молча фейлится (в консоль пишется предупреждение). -
Объект у которого при создании не указан родитель живет в потоке который его создал.
-
При необходимости поток можно менять вызовом
QObject::moveToThread
(переместить объект в поток). Перемещать можно только верхнеуровневых «родителей» (у которыхparent == null
), попытка переместить любого «ребенка» будет молча проигнорирована. -
При перемещении верхнеуровневого «родителя» все его «дети» тоже переедут в новый поток.
-
Получить «текущий» поток исполнения можно через вызов функции
QThread::currentThread()
, поток с которым ассоциирован объект — через вызовQObject::thread()
. -
Все GUI-объекты кроме rendering back-end должны жить в GUI-потоке.
-
GUI-потоком является тот в котором был создан объект
QApplication
.
Таймеры в потоках
Таймеры QTimer
в потоках имеют ряд особенностей, которые нужно учитывать
при проектировании многопоточных приложений на Qt:
-
QTimer
работает только в потоке с event loop (циклом обработки событий). Это значит, что если вы хотите использоватьQTimer
вQThread
, то в этом потоке должен быть запущенQEventLoop
. Если нет цикла событий — таймер просто не сработает. -
QTimer должен быть создан в том потоке, в котором будет работать. Таймер "привязан" к потоку через QObject::thread(). Если вы создадите таймер в основном потоке, а потом попытаетесь использовать его в другом — это будет ошибка.
-
Если объект с таймером перемещается в другой поток через
moveToThread()
, таймер должен быть создан после перемещения. Иначе он останется привязанным к исходному потоку.
Правильная работа с потоками в Qt
Для правильной работы вашего кода в параллельном потоке используйте следующие правила:
- Проектируется отдельный класс (worker), потомок QObject, который будет работать в потоке.
- У этого объекта должны быть определены публичные слоты для запуска и остановки
основной работы, например:
void start();
иvoid stop();
. - Также у объекта должны быть определены сигналы для оповещения после начала
и после полного окончания работы,
например
void started();
иvoid finished();
. - Создание внутренних объектов и таймеров должны выполняться только
в слоте
start()
или позже (не в конструкторе). - Обмен данными с объектом должен осуществляться только через механизм сигнал-слот. Вызов методов напрямую после помещения объекта в поток запрещен!
- Далее в основном потоке программы создаётся экземпляр объекта worker (без родителя),
создаётся оъект
QThread
, и worker перемещвется в поток черезmoveToThread()
. После этого создаются связи сигнал-слот и запускается поток на выполнение.
Пример класса Worker
. Заголовочный файл worker.h
:
#pragma once
#ifndef WORKER_H_
#define WORKER_H_
#include <QtCore/QObject>
#include <QtCore/QTimer>
class Worker: public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
virtual ~Worker();
signals:
void started();
void finished();
// ... другие сигналы наружу
public slots:
void start();
void stop();
// ...другие слоты для наружи
private:
QTimer *m_timer {nullptr};
// ...
private slots:
void timerTimeOut();
// ...
};
#endif // WORKER_H_
Файл с реализацией worker.cpp
:
#include "worker.h"
Worker::Worker(QObject *parent) : QObject{parent}
{
// конструктор выполняется еще в основном потоке!
}
Worker::~Worker()
{
// К моменту вызова деструктора поток уже должен быть остановлен!
// но проверить стоит:
stop();
}
void Worker::start()
{
// К этому моменту мы уже в параллельном потоке
if (m_timer) { // если таймер уже есть, значит это повторный вызов
return;
}
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &Worker::timerTimeOut);
// ... другие действия, например настройки и запуск таймера
m_timer->start(1000);
emit started(); // оповещение, что мы стартанули
}
void Worker::stop()
{
// Тут мы еще в параллельном потоке
if (!m_timer) { // если таймера нет, значит это повторный вызов
return;
}
m_timer->stop();
delete m_timer;
m_timer = nullptr;
// ... тут остановка всей работы, удаление объектов и т.п.
emit finished(); // оповещение, что мы остановились
}
void Worker::timerTimeOut()
{
// Обработчик таймера срабатывает уже в параллельном потоке
// ...
}
Как применить из основного потока, например слот клика по кнопке:
#include "worker.h"
#include <QtCore/QThread>
// ...
void MainWindow::pushButtonClicked()
{
Worker *worker = new Worker(); // без родителя
QThread *thread = new QThread(this);
worker->moveToThread(thread);
// Объект начнёт работу после запуска потока:
connect(thread, &QThread::started, worker, &Worker::start);
// Какой-нибудь сигнал для остановки потока:
connect(this, &MainWindow::stopThread, worker, &Worker::stop);
// Когда объект закончит работу, можно останавливать поток:
connect(worker, &Worker::finished, thread, &QThread::quit);
// Когда поток завершится, можно удалить объект и класс потока:
connect(thread, &QThread::finished, worker, &Worker::deleteLater);
connect(thread, &QThread::finished, thread, &Worker::deleteLater);
// ... тут другие нужные сигналы-слоты
// Запуск потока:
thread->start();
}
// когда поток больше не нужен, вызываем emit stopThread();
Обратите внимание, в примере выше все действия по созданию и запуску потока находятся
в одном слоте и после выхода из слота доступны только через сигналы.
Нет защиты от повторного запуска - т.е. сколько раз нажали кнопку, столько потоков и создаётся,
а остановка всех по сигналу stopThread()
.
Для отслеживания количества нужно добавить счетчик запуска или флаг, например. И при закрытии вашего приложения нужно обязательно дождаться завершения всех созданных потоков! Но это уже на вашей совести.
p.s. Код не проверял и писал по памяти. Возможны очепятки.