![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()` ), второй ограничивается тем что обрабатывает сообщения ранее накопившиеся в очереди.
Итак, как мы уже разобрались, каждый объект в 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)