Qt之多线程、信号和槽

:由于本人是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线程中

  • 17
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Qt中,信号是一种用于对象间通信的机制,可以在多线程环境下使用。通过信号,一个对象可以发射信号,而其他对象可以连接到这个信号并执行相应的函数。 在多线程中使用信号时,需要注意以下几点: 1. 当信号的发送与对象的函数的执行在不同线程中时,可能会产生临界资源的竞争问题。因此,需要使用线程间同步机制来保护共享资源的访问。 2. 在Qt中,QThread继承自QObject,因此可以使用发射信号和定义函数的能力。QThread默认声明了几个关键信号: - started():线程开始运行时发射的信号。 - finished():线程完成运行时发射的信号。 - terminated():线程被异常终止时发射的信号。 下面是一个示例代码,演示了在Qt中如何使用信号进行多线程通信: ```cpp #include <QThread> #include <QDebug> // 自定义线程类 class MyThread : public QThread { Q_OBJECT public: void run() override { qDebug() << "Thread started"; // 执行一些耗时操作 // ... // 发射信号 emit mySignal("Hello from thread"); qDebug() << "Thread finished"; } signals: void mySignal(const QString& message); }; // 自定义函数 class MyObject : public QObject { Q_OBJECT public slots: void mySlot(const QString& message) { qDebug() << "Received message:" << message; } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); MyThread thread; MyObject object; // 连接信号 QObject::connect(&thread, SIGNAL(mySignal(QString)), &object, SLOT(mySlot(QString))); // 启动线程 thread.start(); return a.exec(); } ``` 这段代码中,我们创建了一个自定义的线程类`MyThread`,其中重写了`run()`函数,在函数中执行一些耗时操作,并发射了一个自定义的信号`mySignal`。然后,我们创建了一个自定义的对象`MyObject`,其中定义了一个函数`mySlot`,用于接收信号并处理。在`main()`函数中,我们创建了线程对象和对象对象,并使用`QObject::connect()`函数将信号连接起来。最后,启动线程并运行Qt事件循环。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值