如何正确的使用QThread?(QThread入门)

2013.8.5

翻译自:

http://blog.debao.me/2013/08/how-to-use-qthread-in-the-right-way-part-1/

历史沿革

很久以前,子类化QThread并重新实现其run()函数是唯一推荐使用QThread的方法。 这非常直观,也好用。 但是,qt线程引入SLOTSQt事件循环后,这个方法就有问题了。详见20106.17的这篇文章:https://blog.qt.io/blog/2010/06/17/youre-doing-it-wrong/

于是Qt核心开发人员之一Bradley T. Hughes建议使用QObject :: moveToThread,把对象移动到线程里。 但挺多qt用户不喜欢这种做法。 所以,前Qt核心开发人员之一的Olivier Goffart告诉想子类化QThread的用户也不是不可以,就提供了一个方法,见文章:https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html。所以现在,我们可以在QThread的文档中找到两个用法。

 

线程入口点QThread :: run()

Qt文档里这样说的:

 

QThread实例表示一个线程,调用start()方法启动线程后,执行QThread :: run()里实现的内容。线程的run()相当于应用程序的main()

 

那子类化QThread的用法非常简单明了。

 

子类化QThread 方法

1、来个例子先:

#include

 

class Thread : public QThread

{

private:

    void run()

    {

        qDebug()<<"From worker thread: "<

}

};

int main(int argc, char *argv[])

{

    QCoreApplication a(argc, argv);

    qDebug()<<"From main thread: "<

 

    Thread t;

    QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));

    t.start();

return a.exec();

}

 

程序运行结果大概是这样:

From main thread:  0x15a8

From worker thread:  0x128c

 

2、函数总在调用者的上下文中执行

因为QThread :: run()是入口点,所以很容易理解只有在run()中的代码会在线程中执行。

 

下面的例子stop()在主线程中执行run()在工作线程中运行这俩函数又都使用m_stop成员变量,所以用个互斥

 

#if QT_VERSION>=0x050000

#include

#else

#include

#endif

 

class Thread : public QThread

{

    Q_OBJECT

public:

Thread():m_stop(false)

{}

 

public slots:

    void stop()

    {

        qDebug()<<"Thread::stop called from main thread: "<

        QMutexLocker locker(&m_mutex);

        m_stop=true;

    }

private:

    QMutex m_mutex;

    bool m_stop;

 

    void run()

    {

        qDebug()<<"From worker thread: "<

        while (1) {

            {

            QMutexLocker locker(&m_mutex);

            if (m_stop) break;

            }

            msleep(10);

        }

}

};

 

#include "main.moc"

int main(int argc, char *argv[])

{

    QApplication a(argc, argv);

    qDebug()<<"From main thread: "<

    QPushButton btn("Stop Thread");

    Thread t;

 

    QObject::connect(&btn, SIGNAL(clicked()), &t, SLOT(stop()));

    QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));

 

    t.start();

    btn.show();

return a.exec();

}

 

输出是这样的:

From main thread:  0x13a8

From worker thread:  0xab8

(点击一下按钮)

Thread::stop called from main thread:  0x13a8

 

结论:可以看出来Thread::stop是在主线程中运行的。

 

3、子类化QThread的错误用法

 

上面的例子很容易理解,但是当在工作线程中引入事件系统(或排队连接)时,并不那么直观。

 

例如,如果我们想在工作线程中做某些工作,我们该怎么办?

 

再来一个例子:

首先在Thread :: run()中创建一个QTimer,然后将timeout信号连接到线程对象的槽。

#include

class Thread : public QThread

{

Q_OBJECT

private slots:

    void onTimeout()

    {

        qDebug()<<"Thread::onTimeout get called from? : "<

    }

private:

    void run()

    {

        qDebug()<<"From worker thread: "<

        QTimer timer;

        connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));

        timer.start(1000);

 

        exec();

}

};

 

#include "main.moc"

int main(int argc, char *argv[])

{

    QCoreApplication a(argc, argv);

    qDebug()<<"From main thread: "<

 

    Thread t;

    t.start();

 

return a.exec();

}

 

乍一看,代码似乎很好。 当线程开始执行时,我们设置一个在当前线程的事件队列中运行的QTimer。 我们将onTimeout()连接到timeout信号。 我们希望onTimeout()能在工作线程中执行。

 

但是,例子的结果是

From main thread:  0x13a4

From worker thread:  0x1330

Thread::onTimeout get called from?:  0x13a4

Thread::onTimeout get called from?:  0x13a4

Thread::onTimeout get called from?:  0x13a4

 

F**K!!! onTimeout()在主线程中运行的,没有在工作线程!

 

4、怎么解决这个问题?(2个误用的解决方法)

 

为了使这个槽在工作线程中工作,有人在connect()里增加Qt::DirectConnection像这样:

 

 connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()), Qt::DirectConnection);

 

也有人在线程类的构造函数里加:

 

moveToThread(this);

 

这两个方法都有效果,但是!在构造函数里加moveToThread(this);是很严重的错误!虽然也有效果,但是QThread不是这么设计的,QThread对象的成员函数,应该从创建线程调用,而不是QThread对象自己去调用。按照这个思想,在connect里加Qt::DirectConnection也是错的,onTimeout()是线程对象的成员,也应从创建线程调用。

 

5、正确的解决办法

 

因为QThread对象的成员都没有被设计为从自身线程调用。 因此,如果要让timeout的槽在工作线程中,我们需要创建一个独立的worker对象。

 

#include

class Worker : public QObject

{

Q_OBJECT

private slots:

    void onTimeout()

    {

        qDebug()<<"Worker::onTimeout get called from?: "<

}

};

class Thread : public QThread

{

    Q_OBJECT

private:

    void run()

    {

        qDebug()<<"From work thread: "<

        QTimer timer;

        Worker worker;

        connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));

        timer.start(1000);

 

        exec();

}

};

#include "main.moc"

int main(int argc, char *argv[])

{

    QCoreApplication a(argc, argv);

    qDebug()<<"From main thread: "<

 

    Thread t;

    t.start();

 

return a.exec();

}

 

其实,就是在工作线程中创建一个worker对象,这个对象自然在工作线程中活动。看看运行结果:

From main thread:  0x810

From work thread:  0xfac

Worker::onTimeout get called from?:  0xfac

Worker::onTimeout get called from?:  0xfac

Worker::onTimeout get called from?:  0xfac

 

问题解决!

 

虽然这样很ok,但你注意到没有,从代码设计上,创建worker这个对象其实没必要在run()里。因为不管是worker对象还是QTimer对象,我只是想让它们运行在这个工作线程里,而已。

 

所以我们可以试试将worker对象创建从QThread :: run()中移出。

 

worker对象移到线程中1

 

如果我们对工作线程要求很少,那么完全不需要对QThread进行子类化。QThread自己就有事件循环机制,QThread :: run()默认调用QThread :: exec()。如果自己子类化QThread,还想有事件循环,那就要显式调用QThread :: exec()。

 

#include

class Worker : public QObject

{

Q_OBJECT

private slots:

    void onTimeout()

    {

        qDebug()<<"Worker::onTimeout get called from?: "<

}

};

 

#include "main.moc"

int main(int argc, char *argv[])

{

    QCoreApplication a(argc, argv);

    qDebug()<<"From main thread: "<

 

    QThread t;

    QTimer timer;

    Worker worker;

 

    QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));

    timer.start(1000);

 

    timer.moveToThread(&t);//可注释掉

    worker.moveToThread(&t);

 

    t.start();

 

return a.exec();

}

 

这样做是不是清爽很多。

 

还有更优雅的办法!

 

worker对象移到线程中2

 

 

还有更优雅的办法!

timer.moveToThread(&t);这句也可以注释掉,也就是不需要把QTimer移到工作线程中。区别在于这个方法中,

1timeout()信号发自主线程

2timer对象活动在主线程,worker对象活动在工作线程

3timer对象和worker对象不在一个线程,他们的connect不是直接的,而是排队的。

4worker对象的槽也是在工作线程中运行。

 

能这么做,是因为Qt的排队连接(queued connections)机制。支持跨线程连接信号槽。如果线程同步都是通过这种机制完成,那常见的多线程问题,比如死锁,就交给Qt好了,不需自己费心了。

 

总结

 

子类化QThread并重新实现其run()函数很直观,并且有许多业务场景要这么做。但是当在工作线程中使用事件循环时,一定要用正确的方法。

 

一个简单又正确的办法就是将worker对象移动到工作线程,因为它替Qt开发人员隐蔽了事件循环和排队连接(queued connections)的细节。

 

参考

http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/

http://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html

http://ilearnstuff.blogspot.com/2012/08/when-qthread-isnt-thread.html

发表于Debao Zhang Mon05 Aug 2013 Qt QThread

你可能感兴趣的:(Qt)