如何正确实现QThread?(请举个例子)

73

Qt的QThread文档说要从QThread创建一个类,并实现run方法。

以下是从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开发者的意见,所以乍一看我很感兴趣,经过进一步思考,我同意他的观点。按照面向对象的原则,你真的只想子类化一个类来进一步增强那个类......而不是仅仅直接使用类的方法...这就是为什么你要实例化的原因......

假设我想将自定义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中进行线程处理的合适示例。


4
该文档仅表示不建议在QThread子类中引入新的插槽,没有提到从QThread类派生。从QThread派生遵循Delphi / C++ Builder的TThread相同的模式。 - Zach Saw
3
http://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html - Zach Saw
4
除非你将第一个“connect”行修改为workerThread的*地址,否则他们的示例代码将无法编译,修改如下:connect(&workerThread, SIGNAL(finished()), worker, SLOT(deleteLater())); - Vern Jensen
2
我认为你应该在WorkerObject中删除“QThread workerThread;”,这会导致误解。 - kien bui
5个回答

32

关于这个主题,我能想到的唯一补充是进一步说明QObject与单个线程相关联。通常情况下,这个线程就是创建QObject的线程。因此,如果您在应用程序的主线程中创建了一个QObject并想在另一个线程中使用它,则需要使用moveToThread()来更改关联关系。

这样可以避免子类化QThread并在run()方法中创建对象,从而保持代码的封装性。

该博客文章包含一个示例链接。它很简短,但展示了基本思路。创建你的QObject、连接信号、创建QThread、将你的QObject移动到QThread并启动线程。信号/槽机制将确保线程边界被正确且安全地跨越。

如果您必须在该机制之外调用对象的方法,则可能需要引入同步。

我知道Qt还有一些不错的线程工具,超越了线程,可能值得熟悉,但我还没有做到 :)


1
链接的示例还说明他们会创建 QThread 的子类并实现 run() 方法来执行 exec()。这基本上会启动事件循环,并使连接完成它们的工作... 从我的理解来看,你不应该需要这样做(从我列出的原始帖子中),或者我误解了,你仍然需要这样做吗? - g19fanatic
2
你理解得很正确。从Qt 4.4开始,run()的默认实现已经为你做了这件事。 - Arnold Spence

11

这里有一个正确使用QThread的例子(点击此处),但是它存在一些问题,在评论中有所反映。特别是,由于槽函数执行的顺序不是严格定义的,会导致各种问题。在2013年8月6日发布的评论中提出了一个很好的解决方法。我在我的程序中使用类似的方法,这里有一些示例代码来说明。

基本思路相同:我创建一个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()调用启动新的子进程,维护当前正在运行的进程列表等。但是,它需要跟踪子进程状态,这意味着在Linux上使用waitpid()调用或在Windows上使用WaitForMultipleObjects。我过去使用计时器以非阻塞模式调用它们,但现在我想要更及时的反应,这意味着阻塞模式。这就是线程的作用。

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()方法在那里执行(使用队列连接调用槽)。

然后,我使用Qt::DirectConnection将ChildProcesses::stopped()信号与ChildrenWatcher::stop()槽连接起来,因为我需要异步执行它。这是必要的,以便当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(),你会看到从监视器的destroyed()信号到线程的quit()槽的连接。这意味着线程在观察者完成时自动完成。完成后,它也会自毁,因为它自己的finished()信号已连接到其deleteLater()槽。

这与Maya发布的想法几乎相同,但由于我使用了自毁惯用语法,因此我不需要依赖于调用顺序。它总是先自我毁灭,然后停止线程,然后它也自我毁灭。我可以在worker中定义一个finished()信号,然后将其连接到其自己的deleteLater(),但这只意味着多连接一个信号。由于我不需要完成()信号用于任何其他目的,因此我选择从worker本身调用deleteLater()。

Maya还提到过不应该在worker的构造函数中分配新的QObjects,因为它们不会存在于您将worker移动到的线程中。我认为仍然可以这样做,因为这就是OOP的工作方式。只需确保所有这些QObjects都是worker的子项(即使用QObject(QObject *)构造函数)-moveToThread()将所有子项与要移动的对象一起移动。如果您真的需要有不是您对象子项的QObjects,则覆盖worker中的moveToThread(),使其也移动所有必要的内容。


虽然我欣赏你展示了基于事件的管理器的实现,但它与这个问题无关。这个问题是关于Qt以前推荐如何实现线程和现在“正确”做法之间的文档差异(当前文档中更好的方法)的。 - g19fanatic
2
@g19,我在谷歌搜索正确使用QThread的方法时发现了你的问题(以及其他几个页面)。只有在实施后,我才意识到这正是我正在寻找的。因此,我发布了它,希望其他搜索“正确使用QThread”的人们会发现这很有用。 - Sergei Tachenov
@g19,哦,我当时使用的是Qt 4.6之类的版本,所以不知道它们已经更新了文档。但是文档仍然非常有限,并没有解释如何做我需要的事情(以及许多其他人需要的),所以我想这个问题仍然是值得提出的。 - Sergei Tachenov
1
现在他们在SO上推广“提问FAQ并自己回答”的方式来解决这类问题,所以你可以将其转化为一个单独的问题(详细列出你发现的所有陷阱),并获得一些不错的赞 :) - mlvljr
1
我发现你的回答非常有用,但如果你在某个地方(例如Gist)发布完整的代码以获取所有细节,那就更好了。 - parsley72
@parsley,恐怕完整的代码已经被版权保护了。而且并不是由我本人创作的。 - Sergei Tachenov

4
不是要贬低@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();
}

4
子类化qthread类仍然会在原始线程中运行代码。 我想在已经使用GUI线程(主线程)的应用程序中运行一个udp监听器,尽管我的udp监听器工作得很好,但我的GUI被冻结了,因为它被子类化的qthread事件处理程序阻塞了。 我认为g19fanatic发布的内容是正确的,但您还需要工作线程才能成功将对象迁移到新线程。 我发现这篇文章详细描述了QT中线程编程的Do's和Dont's。

在您决定子类化QThread之前,请务必阅读!


2
不是真的。在重写的函数run()中运行的代码将在新线程上运行。 - Vincent
1
来自 Qt 文档:要记住的重要一点是,QThread 实例存在于实例化它的旧线程中,而不是调用 run() 的新线程中。这意味着所有 QThread 的排队槽将在旧线程中执行。 - Vincent

1

我在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();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接