Qt_Code_Style/QtThreads.md

12 KiB
Raw Permalink Blame History

picture

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. Код не проверял и писал по памяти. Возможны очепятки.