2013.8.5
翻译自:
http://blog.debao.me/2013/08/how-to-use-qthread-in-the-right-way-part-1/
历史沿革
很久以前,子类化QThread并重新实现其run()函数是唯一推荐使用QThread的方法。 这非常直观,也好用。 但是,qt线程引入SLOTS和Qt事件循环后,这个方法就有问题了。详见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 <QtCore>
class Thread : public QThread
{
private:
void run()
{
qDebug()<<"From worker thread: "<<currentThreadId();
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug()<<"From main thread: "<<QThread::currentThreadId();
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 <QtWidgets>
#else
#include <QtGui>
#endif
class Thread : public QThread
{
Q_OBJECT
public:
Thread():m_stop(false)
{}
public slots:
void stop()
{
qDebug()<<"Thread::stop called from main thread: "<<currentThreadId();
QMutexLocker locker(&m_mutex);
m_stop=true;
}
private:
QMutex m_mutex;
bool m_stop;
void run()
{
qDebug()<<"From worker thread: "<<currentThreadId();
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: "<<QThread::currentThreadId();
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 <QtCore>
class Thread : public QThread
{
Q_OBJECT
private slots:
void onTimeout()
{
qDebug()<<"Thread::onTimeout get called from? : "<<QThread::currentThreadId();
}
private:
void run()
{
qDebug()<<"From worker thread: "<<currentThreadId();
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: "<<QThread::currentThreadId();
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 <QtCore>
class Worker : public QObject
{
Q_OBJECT
private slots:
void onTimeout()
{
qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
}
};
class Thread : public QThread
{
Q_OBJECT
private:
void run()
{
qDebug()<<"From work thread: "<<currentThreadId();
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: "<<QThread::currentThreadId();
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 <QtCore>
class Worker : public QObject
{
Q_OBJECT
private slots:
void onTimeout()
{
qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
}
};
#include "main.moc"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug()<<"From main thread: "<<QThread::currentThreadId();
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移到工作线程中。区别在于这个方法中,
1、timeout()信号发自主线程
2、timer对象活动在主线程,worker对象活动在工作线程
3、timer对象和worker对象不在一个线程,他们的connect不是直接的,而是排队的。
4、worker对象的槽也是在工作线程中运行。
能这么做,是因为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 Mon,05 Aug 2013 Qt QThread