在平时的编码过程中经常碰到QT的多线程问题,也大量接触了QT中的两种主流多线程写法,一种是继承QThread类并重载run函数,在run函数中写一个状态机或者计时器来实现对线程运作;一种是通过moveToThread的方式实现事件托管从而实现线程运作。前者虽然是传统写法但是弊病较多,这里主要针对后者来进行说明与理解。
1.常见情况分析:该方法在进行代码编写时的常见情况如下:
class tempActive
{
public:
tempActive()
{
...
a = new A;
m_thread = new QThread;
a->moveToThread(m_thread);
...
]
private:
A* a;
QThread* m_thread;
...
};
上述代码中,tempActive为某一工作相关类,A为某一自定义类或预定义类,其实例a与实例m_thread往往在tempActive的构造函数中实现,并在其中将a实例moveToThread至m_thread线程之中。这里需要着重理解一点,并非是将a实例相关的所有的工作“移动”到了m_thread线程,而是将所有a实例相关的事件托管到m_thread线程执行。换句话说,就是通过信号槽connect或者invokeMethod触发a实例中槽函数产生的事件,将会被放置到m_thread线程中执行,从而实现了多线程工作。也就是说,此时通过a->show()等方式调用直接调用a实例中的函数,无法实现多线程功能。
2.实验验证:为了验证以及加强理解上述结论,在这里做一个简单的实验如下:
class A : public QObject
{
Q_OBJECT
public:
public slots:
void show()
{
qDebug() << "show:" << QThread::currentThreadId();
}
};
class MainWindow : public QObject
{
Q_OBJECT
public:
explicit MainWindow(QObject *parent = 0);
~MainWindow();
public slots:
void mainShow() {qDebug() << "mainShow:" << QThread::currentThreadId();}
signals:
void s_test();
private:
A* a;
QTimer* m_timer;
QThread* m_thread;
};
MainWindow::MainWindow(QObject *parent) :
QObject(parent)
{
a = new A;
m_thread = new QThread;
m_timer = new QTimer;
m_timer->setInterval(500);
m_thread->start();
qDebug() << "A before move:" << QThread::currentThreadId(); //A before move: 0x2a0c
a->show(); //show: 0x2a0c
QMetaObject::invokeMethod(a,"show",Qt::DirectConnection); //show: 0x2a0c
QObject::connect(this,SIGNAL(s_test()),a,SLOT(show()),Qt::DirectConnection);
emit s_test(); //show: 0x2a0c
QObject::disconnect(this,SIGNAL(s_test()),a,SLOT(show()));
a->moveToThread(m_thread);
qDebug() << "A after move:" << QThread::currentThreadId(); //A after move: 0x2a0c
a->show(); //show: 0x2a0c
QMetaObject::invokeMethod(a,"show",Qt::BlockingQueuedConnection); //show: 0x3a9c
QObject::connect(this,SIGNAL(s_test()),a,SLOT(show()),Qt::BlockingQueuedConnection);
emit s_test(); //show: 0x3a9c
QObject::disconnect(this,SIGNAL(s_test()),a,SLOT(show()));
qDebug() << "m_timer before move:" << QThread::currentThreadId(); //m_timer before move: 0x2a0c
m_timer->moveToThread(m_thread);
this->moveToThread(m_thread);
qDebug() << "m_timer after move:" << QThread::currentThreadId(); //m_timer after move: 0x2a0c
QObject::connect(m_timer,SIGNAL(timeout()),this,SLOT(mainShow()),Qt::DirectConnection);
QMetaObject::invokeMethod(m_timer,"start",Qt::BlockingQueuedConnection); //mainShow: 0x3a9c
}
在上述函数中,通过MainWindow的构造函数进行实验,这里分步分析下:
- 当实例a还未移动至m_thread之前,其所有的相关操作均处于当其被new时所处的线程,由于MainWindow在主线程中被构造,所以此时实例a位于主线程0x2a0c。此时通过任何方式,无论直接调用a_>show(),抑或是通过信号槽和invokeMethod调用show(),最终触发时show()必然在主线程0x2a0c之中。
- 当实例a被移动至m_thread之后,其相关事件被托管,但是该实例本身仍位于主线程0x2a0c中,但是通过信号槽和invokeMethod调用show()时发生在m_thread线程0x3a9c之中。
- 当m_timer与this被移动至m_thread之后,其所处线程仍不变为主线程0x2a0c,此时m_timer与this为同一线程,所以信号槽连接时使用DirectConnection,并且需要通过invokeMethod方式来调用m_timer的start方可在主线程0x2a0c中调用m_thread中的m_timer.start()函数(不然会报QTimer无法在另外线程打开的错误,这其实是QT对于QTimer机制的一种保护,有兴趣的读者可以研究下这里将QTimer进行moveToThread之后对于线程的限制保护机制),最后执行的mainShow()函数也确确实实的都在m_thread的线程0x3a9c中执行。
3.结论总结
- moveToThread方式并非“无脑”移动,是一种事件的托管;
- 事件的产生只能通过信号槽或invokeMethod方式来实现,其他的方式(如直接调用)将会导致所调用函数在进行直接调用的线程之中执行,而非moveToThread之后的线程。
- 由于QTimer操作对于线程的限制性,需要保证执行QTimer的相关函数时,如果想采用直接调用则需保证调用函数线程与QTimer所处线程一致,否则就必须使用信号槽或invokeMethod的方式来调用QTimer相关的函数。
- “QT官方文档moveToThread的Warning”:moveToThread往往只能将当前对象从其当前所处线程“推”到某一线程中去(比如1中的常见情况,即在构造函数中将实例a从构建tempActive的线程“推”到了m_thread线程中),不能将当前对象从某线程中“拉”到该线程(即无法在实例a不处在的线程中执行对实例a的moveToThread操作,因为无法在另一线程控制a的自身线程)