注:由于本人是Qt初学者,本篇文章的结论是在了解了Qt多线程相关知识的基础上,又写了简单的代码测试之后得出的结论,如有错误请指正。
本篇文章中的"事件"和"槽函数"等价。仅限于本篇文章。
由于多线程的使用的研究要用到槽函数,槽函数的研究又要用到多线程,所以里面的叙述可能有一点跳跃性,但是看完之后,最后的总结就又会一下子清晰很多。也可以直接先看总结。
一、Qt的多线程
1.多线程基本知识
如果写了一个类,想要将该类实例化对象,并且想让对象的行为在子线程中。就需要下面的步骤:
1.继承QThread类(也可以继承QObject类,利用moveToThread将对象的事件放到QThread对象中,也就是一个线程中去执行)
2.重写run函数
①run函数为线程的入口,继承QThread类之后有默认run函数的,而且默认的run函数是开启事件循环的。(与之对应的是for、while循环)。总之一般就是会给个循环,会有循环是为了不让线程自己直接退出。
②线程启动后,会去执行run函数,如果子线程有需要一直不断处理的事情(即不需要外界条件,也就是不需要信号触发),一般都会在run函数中写个死循环,然后将封装好的函数放进去。
③如果子线程的动作需要信号去触发,比如说我需要显示图片,但是我将图片的处理放到的另一个线程中,我需要其他线程处理完图片后发信号来告诉我,然后我再执行"显示图片"的动作,也就是槽函数,那么一般会在run函数里开启事件循环。
④如果②和③的情况都有的话,目前本人掌握了三种解决办法。第一种解决办法是可以考虑将类中的②和③分成两个线程类,一个用while循环,一个用事件循环。第二种解决办法是用while循环,然后在while循环里检测是否有信号发送过来,有的话就执行槽函数。第三种解决办法就是在run函数中开启事件循环而不写while循环,开启事件循环是为了处理槽函数的。那么需要while循环的成员函数怎么办呢?答案就是可以直接将while循环写到该成员函数中,然后将该成员函数的执行另外起一个线程。只看文字的话,对于后两种解决方法的表述不是很清楚,后面两种方法在下面“多线程的写法”中都会给出完整代码示例。
2.多线程写法
注:这里叙述多线程的写法。以及上面提出的当线程类里既需要while死循环又需要事件的解决办法。
1.线程写法:方法一
(1)简单使用
直接继承QThread,然后重写run函数,调用实例化对象的start()函数启动线程。
//QtThreadTest.h:
#include<QThread>
#include<QObject>
#include<QDebug>
class QtThreadTest1:public QThread
{
Q_OBJECT
protected:
//这里run函数开启的是while循环
void run() {
qDebug() << "QtThreadTest1 ThreadID:" << QThread::currentThreadId();
while (1) {
Print();
}
}
private:
void Print() {
qDebug() << "this is QtThreadTest1";
QThread::sleep(1);
}
};
//main.cpp
#include <QtWidgets/QApplication>
#include"QtThreadTest.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main ThreadID:" << QThread::currentThreadId();
QtThreadTest1 tr1;
tr1.start();
return a.exec();
}
下面对既有while循环又有槽函数的这种情况进行解决。将while循环和槽函数封装成两个线程类这个办法就不写代码示例了。主要代码示例展示这两种办法:“用while循环,然后在while循环里检测是否有信号发送过来,有的话就执行槽函数”;“在run函数中开启事件循环而不写while循环,开启事件循环是处理槽函数。同时给有while循的成员函数另外起一个线程”。这两个方法,第一个方法槽函数和run函数都在一个一个线程中,第二个方法就是将槽函数和run函数各用一个线程去执行。
(2)解决办法一:
对槽函数稍作修改,使得操函数中只是修改一个flg,然后run的while()循环中检测到flg变化,在run函数中执行真正的槽函数。比如原本代码是下面这样的:
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest2 :public QThread
{
Q_OBJECT
public:
void run() {
qDebug() << "this is QtThreadTest2 run():" << QThread::currentThreadId();
while (1) {
qDebug() << "do something in run";
}
}
public slots:
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
QtThreadTest2 t2;
t2.start();//执行run
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t2, &QtThreadTest2::Print);
ts.emitPrint();//发送信号
return a.exec();
}
上面的代码由于run函数一直忙于执行while循环,所以当槽函数Print被出发时,是不会被执行的(其实这里Print是会被执行的,主要是因为这里的槽函数会在主线程中执行,并没有和run函数一样在子线程中执行,这个问题在下面的信号和槽部分进一步叙述,也就是说对于此例子,暂且先持有“槽函数和run在同一个线程”这个错误观点,也就是说暂且认定“由于run函数一直忙于执行while循环,所以当槽函数Print被出发时,是不会被执行的”)。
再来看一下修改后的代码:
class QtThreadTest2 :public QThread
{
Q_OBJECT
public:
void run() {
flg = 0;
qDebug() << "this is QtThreadTest2 run():" << QThread::currentThreadId();
while (1) {
if (flg == 1) {
Print();
flg = 0;
}
qDebug() << "do something in run";
}
}
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
}
public slots:
void Printslot() {
flg = 1;
}
private:
int flg;
};
看一下修改的前后对比:
修改后的槽函数的函数体只是修改了一个flg标志,run函数中的while里检测到flg被置为1后,就执行Print函数,然后将flg重新置为0.这是一个解决办法,但是这个解决办法有个缺点,就是如果while里的"do something in run"是个比较耗时的操作的话(这里默认while内没有嵌套while死循环),那么槽函数被触发后可能不是立即执行。
2.线程写法:方法二
继承QObject类,实例化一个QThread对象,然后利用moveToThread将对象的事件添加到线程对象中。不需要重写run函数。这里是将对象的事件添加到线程中。(槽函数就是事件)
//QtThreadTest.h:
#include<QThread>
#include<QObject>
#include<QDebug>
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest2 :public QObject
{
Q_OBJECT
public slots:
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
#include"QtThreadTest.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
QThread thread;
QtThreadTest2 t2;
t2.moveToThread(&thread);
thread.start();
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t2, &QtThreadTest2::Print);
ts.emitPrint();//发送信号
return a.exec();
}
运行结果:
可以看到槽函数和主线程不在同一个线程内。
而且也说了,无需重写run函数,因为此代码中是将事件添加到子线程中,此时代码thread->start()的线程入口已经不是t2的run函数了。
这里就有个问题了,如果类中既有事件又有需要while死循环的动作怎么办呢?有一个办法就是让t2同时继承QThread类,并且利用t2.moveToThread(&thread)将t2的事件放入到thread线程中,同时执行t2.start()启动run函数,这样就会有两个线程了。
完善代码:
//QtThreadTest.h:
#include<QThread>
#include<QObject>
#include<QDebug>
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest2 :public QObject,public QThread
{
Q_OBJECT
public:
void run() {
qDebug() << "this is QtThreadTest2 run():" << QThread::currentThreadId();
while (1) {
qDebug() << "do something in run";
QThread::sleep(1);
}
}
public slots:
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
#include"QtThreadTest.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
QThread thread;
QtThreadTest2 t2;
t2.QObject::moveToThread(&thread);
thread.start();//将t2的事件放到线程thread中
t2.start();//启动线程执行run
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t2, &QtThreadTest2::Print);
ts.emitPrint();//发送信号
return a.exec();
}
运行结果:
可以看见主线程id,还有一个槽函数执行所在id(t2槽函数执行时所在线程),还有一个t2的run函数所在线程。这样就解决了。
3.线程写法:方法三
这个方法放到线程写法的最后讲,是因为这个写法不被提倡。但是如果没有特殊情况的话本人一般会使用这个写法。主要就是继承Qthread,在构造函数里加上moveToThread(this)这句代码。
代码示例:
//QtThreadTest.h:
#include<QThread>
#include<QObject>
#include<QDebug>
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest3 :public QThread
{
Q_OBJECT
public:
QtThreadTest3()
{
moveToThread(this);//要加QThread::
}
protected:
void run() {
qDebug() << "this is QtThreadTest3 run():" << QThread::currentThreadId();
qDebug() << "do something in run";
//开启事件循环,否则的话会退出线程
//不可以将事件循环改成while循环,否则的话槽函数得不到响应
exec();
}
public slots:
void Print() {
qDebug() << "this is QtThreadTest3 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
#include"QtThreadTest.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
QtThreadTest3 t3;
t3.start();
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t3, &QtThreadTest3::Print);
ts.emitPrint();//发送信号
return a.exec();
}
运行结果:
注意,槽函数和run函数在一个子线程里面。
有个问题就是,槽函数和run函数都在子线程里面,那么当run函数需要while循环的时候,就无法执行被触发的槽函数。上面说了解决办法,就是槽函数只修改flg,run函数while循环检测到flg变化,执行真正要执行的动作。
二、Qt的信号和槽
1.信号和槽的基本知识
(1)信号和槽函数的返回值类型都是void,槽函数形参个数等于或者大于信号的参数个数,且形参类型要从左到右匹配。
(2)信号和槽第二个地方就是要看connect了。connect的第五个参数经常忽略。
static bool connect(const QObject *sender, const char *signal, const QObject
*receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection
第五个参数是自动选择链接方式。有以下链接方式:
Constant Value Description
Qt::AutoConnection 0 (default) If the signal is emitted from a different thread than the receiving object, the signal is queued, behaving as Qt::QueuedConnection. Otherwise, the slot is invoked directly, behaving as Qt::DirectConnection. The type of connection is determined when the signal is emitted.
Qt::DirectConnection 1 The slot is invoked immediately, when the signal is emitted.
Qt::QueuedConnection 2 The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.
Qt::BlockingQueuedConnection 3 Same as QueuedConnection, except the current thread blocks until the slot returns. This connection type should only be used where the emitter and receiver are in different threads.
平时我们不会去写第五个参数。注意Qt::DirectConnection是直接调用。
2.槽函数的执行线程(仅研究信号和槽不在一个线程时)
上面说到一个线程类中,如果run函数中有while死循环的话,槽函数被触发时,可能由于此时线程忙于执行run函数中while循环,而无法去执行槽函数。但是没有给代码示例验证这个说法,主要原因就是这里又涉及到一个知识点,即槽函数执行时,是在哪个线程执行的。这里跟槽函数的代码的写法有关,准确点说,根据槽函数所在线程类的写法有关。特别注意的是下面这个写法:
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest2 :public QThread
{
Q_OBJECT
public:
void run() {
qDebug() << "this is QtThreadTest2 run():" << QThread::currentThreadId();
while (1) {
qDebug() << "do something in run";
}
}
public slots:
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
QtThreadTest2 t2;
t2.start();//执行run
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t2, &QtThreadTest2::Print);
ts.emitPrint();//发送信号
return a.exec();
}
上面拿这个举例时,说槽函数不会执行,因为线程一直忙于run函数的while(1),无法腾出空去执行被触发的槽函数。但是并非如此,因为此时槽函数和run函数并不在同一个线程中。
运行结果:
对于此写法,run函数在子线程中执行,槽函数在run函数所在线程的依赖线程中执行。
总结
多线程和槽函数相遇时的使用
上面多线程介绍时,线程的使用方法情况依次为:
(1)继承QThread类,重写run函数,使用"对象.start()"的方式启动线程,run函数为线程入口。此时槽函数在run函数所在线程的依赖线程中执行。如果在主线程中创建的对象,启动的线程,那么槽函数就在主线程执行,run函数在子线程中执行。
(2)继承QObject类,不重写run函数(重写了也不会执行run函数,因为此时重写的run函数不是新线程的入口)。 用"对象.QObject::moveToThread(&thread)"的方式(thread为QThread的实例化对象)创建新线程(准确的说是将"对象"的事件都给放到线程类对象thread中,内部应该是事件循环,当“对象”的事件被触发时,会执行),用"thread.start()“的方式开始启动新建线程。
(3)继承QThread类和QObject类,重写run函数,然后方法一和方法二的启动操作都执行,槽函数和run函数都在子线程中,且不在同一个子线程。
(4)继承QThread类,在构造函数中加上"moveToThread(this)”,槽函数和run函数都在子线程中,且在同一个子线程中。注意当run函数和槽函数都在同一个子线程中时,run函数内不要有while死循环,否则的话槽函数被触发时,不会被执行。
下面对应着平时使用到的一些场景(假设槽函数不能在主线程执行):
(1)线程类的run函数需要执行一些成员方法和槽函数,但是不存在while死循环
适用的多线程使用方法:
①不适用。但是可以槽函数只用来修改flg,可以在run函数里写一个while循环检测flg是否变化,flg变化了就在while内调用真正需要执行的动作,然后将flg复原(上面讲过这个方法)。这样就适用了。
②不适用。
③适用。
④适用。run函数要开启事件循环。
(2)线程类的run函数需要执行一些成员方法和槽函数,存在while死循环
①不适用。但是可以槽函数只用来修改flg,可以在run函数里写一个while循环检测flg是否变化,flg变化了就在while内调用真正需要执行的动作,然后将flg复原(上面讲过这个方法)。这样就适用了。
②不适用。
③适用。
④不适用。但是可以槽函数只用来修改flg,可以在run函数里写一个while循环检测flg是否变化,flg变化了就在while内调用真正需要执行的动作,然后将flg复原(上面讲过这个方法)。这样就适用了。
(3)线程类的只需要执行一些耗时的成员方法,没有事件(槽函数)
①适用。
②不适用。
③适用。
④适用。
(4)线程类只有事件需要在被触发的时候执行,然后执行事件(槽函数)。
①不适用。run函数开启事件循环也不行,因为在线程使用方法一中,槽函数会在run函数所在线程的依赖线程中执行。
②适用。
③适用。
④适用。
当对多线程有了进一步的了解之后,还是发现Qt的多线程的使用方式还是很多的,而且线程间的使用多多少少都有点区别,上面的总结感觉使用交错复杂,但是只需要记住多线程使用方法③就可以了,因为方法③就是将事件和run函数各起一个线程,互不影响,而且可以只起其中一个线程,灵活性高。最后再附上线程的使用方法③的代码。
多线程使用方法③:继承QThread类和QObject类,重写run函数,然后方法一和方法二的启动操作都执行,槽函数和run函数都在子线程中,且不在同一个子线程。
代码再次举例:
//QtThreadTest.h:
#include<QThread>
#include<QObject>
#include<QDebug>
class QtThreadTestSing :public QObject
{
Q_OBJECT
signals:
void emitPrint();
};
class QtThreadTest2 :public QObject,public QThread
{
Q_OBJECT
public:
void run() {
qDebug() << "this is QtThreadTest2 run():" << QThread::currentThreadId();
while (1) {
qDebug() << "do something in run";
QThread::sleep(1);
}
}
public slots:
void Print() {
qDebug() << "this is QtThreadTest2 Print():" << QThread::currentThreadId();
QThread::sleep(1);
}
};
//main.cpp
#include"QtThreadTest.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug() << "main Thread:" << QThread::currentThreadId();
/
//如果没有事件,就只需要t2.start(),如果只有事件,就只需要thread.start(),
//如果事件(槽函数)和行为(run)都需要,就两个线程启动都执行。
//当只有事件时,也可以在run函数中开启事件循环,执行t2.start()
//...
//使用变化多,灵活性高,所以基本可以在应对所有情况下,代码变化又极少。本质就是可以将事件和run函数各起一个线程
QThread thread;
QtThreadTest2 t2;
t2.QObject::moveToThread(&thread);
thread.start();//将t2的事件放到线程thread中
t2.start();//启动线程执行run
/
QtThreadTestSing ts;
QObject::connect(&ts, &QtThreadTestSing::emitPrint, &t2, &QtThreadTest2::Print);
ts.emitPrint();//发送信号
return a.exec();
}
其实Qt的多线程的用法还有很多,这里给上Qt多线程另一种类型的用法链接。
直接给函数放到Qt线程中。