
- [К списку документов](Readme.md)
# 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`:
```C
#pragma once
#ifndef WORKER_H_
#define WORKER_H_
#include
#include
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`:
```C
#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()
{
// Обработчик таймера срабатывает уже в параллельном потоке
// ...
}
```
Как применить из основного потока, например слот клика по кнопке:
```C
#include "worker.h"
#include
// ...
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. Код не проверял и писал по памяти. Возможны очепятки.*
---
- [К списку документов](Readme.md)