Qt_Code_Style/QtThreads.md

254 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

![picture](/_resources/images/qtcat.png)
- [К списку документов](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/>