как правильно реализовать QThread (пример, пожалуйста)

В документации Qt для QThread говорится о создании класса из QThread и реализации метода запуска.

Ниже взято из документации 4.7 Qthread...

Чтобы создать свои собственные потоки, создайте подкласс QThread и переопределите run(). Например:

 class MyThread : public QThread
 {
 public:
     void run();
 };

 void MyThread::run()
 {
     QTcpSocket socket;
     // connect QTcpSocket's signals somewhere meaningful
     ...
     socket.connectToHost(hostName, portNumber);
     exec();
 }

Таким образом, в каждом отдельном потоке, который я создал, я сделал именно это, и для большинства вещей это работает просто отлично (я не реализую moveToThread(this) ни в одном из моих объектов, и это прекрасно работает).

На прошлой неделе я столкнулся с проблемой (мне удалось преодолеть ее, работая над тем, где я создавал свои объекты) и нашел после публикации в блоге. Здесь в основном говорится, что создание подкласса QThread действительно не правильный способ сделать это (и что документация неверна).

Это исходит от разработчика Qt, поэтому с первого взгляда мне было интересно, и после дальнейшего размышления я согласен с ним. Следуя принципам OO, вы действительно хотите создать подкласс класса только для дальнейшего улучшения этого класса... а не просто напрямую использовать методы классов... вот почему вы создаете экземпляр...

Допустим, я хотел переместить пользовательский класс QObject в поток... как это сделать «правильнее»? В этом сообщении в блоге он «говорит», что у него где-то есть пример... но если бы кто-то мог объяснить мне это, я был бы очень признателен!

Обновление:

Поскольку этому вопросу уделяется так много внимания, вот копия и вставка документации 4.8 с «правильным» способом реализации QThread.

class Worker : public QObject
 {
     Q_OBJECT
     QThread workerThread;

 public slots:
     void doWork(const QString &parameter) {
         // ...
         emit resultReady(result);
     }

 signals:
     void resultReady(const QString &result);
 };

 class Controller : public QObject
 {
     Q_OBJECT
     QThread workerThread;
 public:
     Controller() {
         Worker *worker = new Worker;
         worker->moveToThread(&workerThread);
         connect(workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
         connect(this, SIGNAL(operate(QString)), worker, SLOT(doWork(QString)));
         connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handleResults(QString)));
         workerThread.start();
     }
     ~Controller() {
         workerThread.quit();
         workerThread.wait();
     }
 public slots:
     void handleResults(const QString &);
 signals:
     void operate(const QString &);
 };

Я все еще считаю, что стоит отметить, что они включают дополнительный элемент Worker::workerThread, который не нужен и никогда не используется в их примере. Удалите эту часть, и это будет правильный пример того, как реализовать многопоточность в Qt.


person g19fanatic    schedule 04.11.2010    source источник
comment
В документе только говорится, что введение новых слотов в подкласс QThread не рекомендуется. Не упоминалось о производном от класса QThread. Наследование от QThread следует той же парадигме, что и TThread Delphi/C++ Builder.   -  person Zach Saw    schedule 26.11.2013
comment
comment
Их пример кода не будет скомпилирован, пока вы не измените первую строку «connect» на адрес workerThread, например: connect(&workerThread, SIGNAL(finished()), worker, SLOT(deleteLater() ));   -  person Vern Jensen    schedule 12.07.2014
comment
Я думаю, что вам следует удалить QThread workerThread; в рабочем объекте. Это вызвало недоразумение   -  person kien bui    schedule 07.05.2018


Ответы (5)


Единственное, что я могу добавить, это еще раз указать, что QObject имеют сходство с одним потоком. Обычно это поток, создающий файл QObject. Поэтому, если вы создаете QObject в основном потоке приложения и хотите использовать его в другом потоке, вам нужно использовать moveToThread() для изменения привязки.

Это избавляет от необходимости создавать подклассы QThread и создавать свои объекты в методе run(), таким образом сохраняя ваши вещи хорошо инкапсулированными.

Этот пост в блоге содержит ссылку на пример. Он довольно короткий, но он показывает основную идею. Создайте свои QObject, подключите свои сигналы, создайте свои QThread, переместите свои QObjects в QThread и запустите поток. Механизмы сигнала/слота гарантируют правильное и безопасное пересечение границ потока.

Возможно, вам придется ввести синхронизацию, если вам нужно вызывать методы вашего объекта вне этого механизма.

Я знаю, что в Qt есть и другие полезные средства многопоточности помимо потоков, которые возможно, стоит познакомиться, но я еще не сделал этого :)

person Arnold Spence    schedule 04.11.2010
comment
в связанном примере также говорится, что они создают подкласс QThread и реализуют run() для выполнения exec(). Это в основном запустит цикл событий и позволит соединениям делать свое дело... Из того, что я понял, вам не нужно этого делать (из исходного сообщения, которое я указал), или я неправильно понимаю, и вам все равно нужно будет сделать это? - person g19fanatic; 04.11.2010
comment
Вы правильно понимаете. Начиная с Qt 4.4, реализация run() по умолчанию делает это за вас. - person Arnold Spence; 04.11.2010

Вот один пример как правильно использовать QThread, но с ним есть некоторые проблемы, которые отражены в комментариях. В частности, поскольку порядок выполнения слотов строго не определен, это может привести к различным проблемам. Комментарий, опубликованный 6 августа 2013 г., дает хорошее представление о том, как решить эту проблему. Я использую что-то подобное в своей программе, и вот пример кода для пояснения.

Основная идея та же: я создаю экземпляр QThread, который живет в моем основном потоке, экземпляр рабочего класса, который живет в новом созданном потоке, а затем подключаю все сигналы.

void ChildProcesses::start()
{
    QThread *childrenWatcherThread = new QThread();
    ChildrenWatcher *childrenWatcher = new ChildrenWatcher();
    childrenWatcher->moveToThread(childrenWatcherThread);
    // These three signals carry the "outcome" of the worker job.
    connect(childrenWatcher, SIGNAL(exited(int, int)),
            SLOT(onChildExited(int, int)));
    connect(childrenWatcher, SIGNAL(signalled(int, int)),
            SLOT(onChildSignalled(int, int)));
    connect(childrenWatcher, SIGNAL(stateChanged(int)),
            SLOT(onChildStateChanged(int)));
    // Make the watcher watch when the thread starts:
    connect(childrenWatcherThread, SIGNAL(started()),
            childrenWatcher, SLOT(watch()));
    // Make the watcher set its 'stop' flag when we're done.
    // This is performed while the watch() method is still running,
    // so we need to execute it concurrently from this thread,
    // hence the Qt::DirectConnection. The stop() method is thread-safe
    // (uses a mutex to set the flag).
    connect(this, SIGNAL(stopped()),
            childrenWatcher, SLOT(stop()), Qt::DirectConnection);
    // Make the thread quit when the watcher self-destructs:
    connect(childrenWatcher, SIGNAL(destroyed()),
            childrenWatcherThread, SLOT(quit()));
    // Make the thread self-destruct when it finishes,
    // or rather, make the main thread delete it:
    connect(childrenWatcherThread, SIGNAL(finished()),
            childrenWatcherThread, SLOT(deleteLater()));
    childrenWatcherThread->start();
}

Немного фона:

Класс ChildProcesses — это менеджер дочерних процессов, который запускает новые дочерние процессы с помощью вызовов spawn(), сохраняет список запущенных в данный момент процессов и так далее. Однако ему необходимо отслеживать дочерние состояния, что означает использование вызова waitpid() в Linux или WaitForMultipleObjects в Windows. Раньше я вызывал их в неблокирующем режиме с помощью таймера, но теперь я хочу более быстрой реакции, что означает блокирующий режим. Вот где в дело вступает нить.

Класс ChildrenWatcher определяется следующим образом:

class ChildrenWatcher: public QObject {
    Q_OBJECT
private:
    QMutex mutex;
    bool stopped;
    bool isStopped();
public:
    ChildrenWatcher();
public slots:
    /// This is the method which runs in the thread.
    void watch();
    /// Sets the stop flag.
    void stop();
signals:
    /// A child process exited normally.
    void exited(int ospid, int code);
    /// A child process crashed (Unix only).
    void signalled(int ospid, int signal);
    /// Something happened to a child (Unix only).
    void stateChanged(int ospid);
};

Вот как это работает. Когда все это запускается, вызывается метод ChildProcess::start() (см. выше). Он создает новый QThread и новый ChildrenWatcher, который затем перемещается в новый поток. Затем я подключаю три сигнала, которые информируют моего менеджера о судьбе его дочерних процессов (выход/сигнализация/бог знает, что случилось). Дальше начинается основное веселье.

Я подключаю QThread::started() к методу ChildrenWatcher::watch(), поэтому он запускается, как только поток готов. Поскольку наблюдатель живет в новом потоке, именно там выполняется метод watch() (для вызова слота используется соединение в очереди).

Затем я подключаю сигнал ChildProcesses::stopped() к слоту ChildrenWatcher::stop(), используя Qt::DirectConnection, потому что мне нужно сделать это асинхронно. Это необходимо, чтобы мой поток останавливался, когда диспетчер ChildProcesses больше не нужен. Метод stop() выглядит так:

void ChildrenWatcher::stop()
{
    mutex.lock();
    stopped = true;
    mutex.unlock();
}

И затем ChildrenWatcher::watch():

void ChildrenWatcher::watch()
{
  while (!isStopped()) {
    // Blocking waitpid() call here.
    // Maybe emit one of the three informational signals here too.
  }
  // Self-destruct now!
  deleteLater();
}

Да, и метод isStopped() — это просто удобный способ использовать мьютекс в условии while():

bool ChildrenWatcher::isStopped()
{
    bool stopped;
    mutex.lock();
    stopped = this->stopped;
    mutex.unlock();
    return stopped;
}

Итак, что здесь происходит, так это то, что я устанавливаю флаг остановки, когда мне нужно закончить, а затем при следующем вызове isStopped() он возвращает false, и поток завершается.

Так что же происходит, когда заканчивается цикл watch()? Он вызывает deleteLater(), поэтому объект самоуничтожается, как только управление возвращается в цикл событий потока, который происходит сразу после вызова deleteLater() (когда возвращается watch()). Возвращаясь к ChildProcesses::start(), вы можете видеть, что существует соединение между сигналом destroy() наблюдателя и слотом quit() потока. Это означает, что поток автоматически завершается, когда наблюдатель завершает работу. И когда он закончен, он также самоуничтожается, потому что его собственный сигнал finish() подключен к его слоту deleteLater().

Это в значительной степени та же идея, что и Maya, но поскольку я использую идиому самоуничтожения, мне не нужно зависеть от последовательности, в которой вызываются слоты. Он всегда сначала самоуничтожается, потом останавливает поток, а потом тоже самоуничтожается. Я мог бы определить сигнал finish() в рабочем потоке, а затем соединить его с его собственным deleteLater(), но это означало бы только одно дополнительное соединение. Поскольку мне не нужен сигнал finish() для каких-либо других целей, я решил просто вызвать deleteLater() из самого воркера.

Maya также упоминает, что вы не должны размещать новые объекты QObject в конструкторе рабочего процесса, потому что они не будут жить в потоке, в который вы перемещаете рабочий процесс. Я бы сказал, сделайте это в любом случае, потому что так работает ООП. Просто убедитесь, что все эти объекты QObject являются дочерними элементами рабочего процесса (то есть используйте конструктор QObject(QObject*)) — moveToThread() перемещает все дочерние объекты вместе с перемещаемым объектом. Если вам действительно нужно иметь QObjects, которые не являются дочерними элементами вашего объекта, переопределите moveToThread() в вашем рабочем потоке, чтобы он также перемещал все необходимое.

person Sergei Tachenov    schedule 16.08.2013
comment
хотя я ценю, что вы показали свою реализацию менеджера на основе событий, это не имеет отношения к этому вопросу. Вопрос был связан с несоответствием документации между тем, как Qt использовал рекомендации для реализации потоков, и «правильным» способом сделать это (что теперь лучше в текущей документации)... - person g19fanatic; 19.08.2013
comment
@ g19, я нашел ваш вопрос (и несколько других страниц), когда искал правильный способ использования QThread. Только после этого я реализовал это, а потом понял, что это именно то, что я гуглил. Поэтому я разместил его в надежде, что кто-то еще, кто ищет правильный способ использования QThread, найдет это полезным. - person Sergei Tachenov; 20.08.2013
comment
@ g19 g19, о, и я работал с Qt 4.6 или чем-то подобным, поэтому я понятия не имел, что они изменили документы. Но документы по-прежнему очень ограничены и не объясняют, как сделать то, что мне нужно (и многим другим людям нужно), поэтому я думаю, что вопрос все еще актуален. - person Sergei Tachenov; 20.08.2013
comment
Теперь они как бы продвигают вопрос о вопросах и ответах на вопросы такого типа на SO, так что вы можете преобразовать это в отдельный вопрос (с подробным перечислением всех подводных камней, которые вы нашли), а также получить несколько хороших голосов :) - person mlvljr; 27.10.2013
comment
Я нашел ваш ответ очень полезным, но было бы хорошо, если бы вы разместили где-нибудь полный код (например, Gist), чтобы получить все подробности. - person parsley72; 27.04.2014
comment
@parsley, боюсь, весь код защищен авторским правом. И не совсем сам. - person Sergei Tachenov; 28.04.2014

Не умаляя отличного ответа @sergey-tachenov, но в Qt5 вы можете отказаться от использования SIGNAL и SLOT, упростить свой код и получить преимущество проверки времени компиляции:

void ChildProcesses::start()
{
    QThread *childrenWatcherThread = new QThread();
    ChildrenWatcher *childrenWatcher = new ChildrenWatcher();
    childrenWatcher->moveToThread(childrenWatcherThread);
    // These three signals carry the "outcome" of the worker job.
    connect(childrenWatcher, ChildrenWatcher::exited,
            ChildProcesses::onChildExited);
    connect(childrenWatcher, ChildrenWatcher::signalled,
            ChildProcesses::onChildSignalled);
    connect(childrenWatcher, ChildrenWatcher::stateChanged,
            ChildProcesses::onChildStateChanged);
    // Make the watcher watch when the thread starts:
    connect(childrenWatcherThread, QThread::started,
            childrenWatcher, ChildrenWatcher::watch);
    // Make the watcher set its 'stop' flag when we're done.
    // This is performed while the watch() method is still running,
    // so we need to execute it concurrently from this thread,
    // hence the Qt::DirectConnection. The stop() method is thread-safe
    // (uses a mutex to set the flag).
    connect(this, ChildProcesses::stopped,
            childrenWatcher, ChildrenWatcher::stop, Qt::DirectConnection);
    // Make the thread quit when the watcher self-destructs:
    connect(childrenWatcher, ChildrenWatcher::destroyed,
            childrenWatcherThread, QThread::quit);
    // Make the thread self-destruct when it finishes,
    // or rather, make the main thread delete it:
    connect(childrenWatcherThread, QThread::finished,
            childrenWatcherThread, QThread::deleteLater);
    childrenWatcherThread->start();
}
person Community    schedule 27.04.2014

создание подкласса класса qthread по-прежнему будет запускать код в исходном потоке. Я хотел запустить прослушиватель udp в приложении, которое уже использует поток GUI (основной поток), и хотя мой прослушиватель udp работал отлично, мой графический интерфейс был заморожен, поскольку он был заблокирован подклассами обработчиков событий qthread. Я думаю, что то, что опубликовал g19fanatic, правильно, но вам также понадобится рабочий поток, чтобы успешно перенести объект в новый поток. Я нашел этот пост, в котором подробно описывается, что можно и чего нельзя делать с потоками в QT.

Должен прочитать, прежде чем вы решите создать подкласс QThread!

person Community    schedule 18.08.2014
comment
Не правда. Код, работающий в переопределенной функции run(), будет выполняться в новом потоке. - person Vincent; 04.05.2017
comment
Из документов Qt: важно помнить, что экземпляр QThread живет в старом потоке, создавшем его экземпляр, а не в новом потоке, который вызывает run(). Это означает, что все слоты очереди QThread будут выполняться в старом потоке. - person Vincent; 04.05.2017

Моя версия передовой модели потоков в Qt5 проста: worker.h:

/*
* This is a simple, safe and best-practice way to demonstrate how to use threads in Qt5.
* Copyright (C) 2019 Iman Ahmadvand
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

#ifndef _WORKER_H
#define _WORKER_H

#include <QtCore/qobject.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qthread.h>
#include <QtGui/qevent.h>

namespace concurrent {

    class EventPrivate;
    class Event : public QEvent {
    public:
        enum {
            EventType1 = User + 1
        };

        explicit Event(QEvent::Type);
        Event(QEvent::Type, const QByteArray&);

        void setData(const QByteArray&);
        QByteArray data() const;

    protected:
        EventPrivate* d;
    };

    class WorkerPrivate;
    /* A worker class to manage one-call and permanent tasks using QThread object */
    class Worker : public QObject {
        Q_OBJECT

    public:
        Worker(QThread*);
        ~Worker();

    protected slots:
        virtual void init();

    protected:
        bool event(QEvent*) override;

    protected:
        WorkerPrivate* d;

    signals:
        /* this signals is used for one call type worker */
        void finished(bool success);
    };

} // namespace concurrent

#endif // !_WORKER_H

worker.cpp:

/*
* This is a simple, safe and best-practice way to demonstrate how to use threads in Qt5.
* Copyright (C) 2019 Iman Ahmadvand
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

#include "worker.h"

using namespace concurrent;

class concurrent::EventPrivate {
public:
    QByteArray data;
};

Event::Event(QEvent::Type t):QEvent(t), d(new EventPrivate) {
    setAccepted(false);
}

Event::Event(QEvent::Type t, const QByteArray& __data) : Event(t) {
    setData(__data);
}

void Event::setData(const QByteArray& __data) {
    d->data = __data;
}

QByteArray Event::data() const {
    return d->data;
}



class concurrent::WorkerPrivate {
public:
    WorkerPrivate() {

    }
};

Worker::Worker(QThread* __thread) :QObject(nullptr), d(new WorkerPrivate) {
    moveToThread(__thread);

    QObject::connect(__thread, &QThread::started, this, &Worker::init);
    QObject::connect(this, &Worker::finished, __thread, &QThread::quit);
    QObject::connect(__thread, &QThread::finished, __thread, &QThread::deleteLater);
    QObject::connect(__thread, &QThread::finished, this, &Worker::deleteLater);
}

Worker::~Worker() {
    /* do clean up if needed */
}

void Worker::init() {
    /* this will called once for construction and initializing purpose */
}

bool Worker::event(QEvent* e) {
    /* event handler */
    if (e->type() == Event::EventType1) {
        /* do some work with event's data and emit signals if needed */
        auto ev = static_cast<Event*>(e);
        ev->accept();
    }
    return QObject::event(e);
}

usage.cpp:

#include <QtCore/qcoreapplication.h>
#include "worker.h"

using namespace concurrent;

Worker* create(bool start) {
    auto worker = new Worker(new QThread);
    if (start)
        worker->thread()->start();

    return worker;
}

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
    auto worker = create(true);
    if (worker->thread()->isRunning()) {
        auto ev = new Event(static_cast<QEvent::Type>(Event::EventType1));
        qApp->postEvent(worker, ev, Qt::HighEventPriority);
    }
    return app.exec();
}
person IMAN4K    schedule 14.10.2019