254 lines
12 KiB
Markdown
254 lines
12 KiB
Markdown

|
||
|
||
- [К списку документов](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()` ),
|
||
второй ограничивается тем что обрабатывает сообщения ранее накопившиеся в очереди.
|
||
|
||
<br/>
|
||
|
||
Итак, как мы уже разобрались, каждый объект в 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()`.
|
||
После этого создаются связи сигнал-слот и запускается поток на выполнение.
|
||
|
||
<br/>
|
||
|
||
Пример класса `Worker`. Заголовочный файл `worker.h`:
|
||
|
||
```C
|
||
#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`:
|
||
|
||
```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 <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()`.
|
||
|
||
<br/>
|
||
|
||
Для отслеживания количества нужно добавить счетчик запуска или флаг, например.
|
||
И при закрытии вашего приложения нужно обязательно дождаться завершения всех
|
||
созданных потоков!
|
||
Но это уже на вашей совести.
|
||
|
||
<br/>
|
||
|
||
*p.s. Код не проверял и писал по памяти. Возможны очепятки.*
|
||
|
||
<br/>
|
||
|
||
---
|
||
|
||
<br/>
|
||
|
||
- [К списку документов](Readme.md)
|
||
|
||
<br/>
|
||
|
||
<br/>
|
||
|