QT信号的种类及使用、常见问题分析

1.基本概念——信号的处理方式

  1. 每一个线程都有自己的事件队列
  2. 线程通过事件队列接收信号
  3. 信号在事件队列里被处理

请添加图片描述

2.信号种类的区分

查看Connect函数的声明
请添加图片描述

可以发现第5个参数即为信号的种类。
点进去去看,其实是一个枚举类
请添加图片描述
查看QT帮助文档
请添加图片描述

第一句话的意思大致为:这个枚举类描述了信号和槽之间的连接方式,特别是它决定了信号发送后槽函数是立刻响应还是排队等候响应。
从上到下依次是:自动连接、直接连接、队列连接、阻塞连接、唯一连接。

3. QT连接方式

3.1 直接连接方式:DirectConnection

发送信号的时候关联的槽函数被发送信号的线程所调用-立即执行(无论这个槽函数所属的对象依附哪个线程) 。该连接方式不需要启动事件循环方式,会让对应的槽函数在发射信号的线程中进行。但是注意信号和槽要在同一个线程中,否则线程不安全。

请添加图片描述

3.1.1为什么在不同线程的信号和槽会不安全

1.会打破原有的顺序

比如跨线程的信号,原来是使用队列方式,现在突然使用直接连接,就会“插队执行”

2.可能造成主线程、UI界面的卡顿

如果槽函数执行的是耗时操作,会导致卡顿

3.可能出现线程安全问题

一个线程的资源可能由主线程、子线程同时访问,存在安全问题

3.1.2 使用场景

1.需要立刻终止某些操作:比如安全方面,需要立刻停止数据输出、冻结账户等。通过此种方法可以“插队执行”。
2.是否可以通过这种方式节约时间? 比如一些不存在安全问题的简单操作,是否可以通过此种方式避免排队?

3.1.3直连信号的执行顺序

如果两个信号都是直连,那么会按照QT的任务调度,先看哪个signal先发出,再看哪个connect先建立。

3.2 排队连接方式:QueuedConnection

就是信号发出后等待QT的调度(QT是有自己的事件循环的),排到他以后再执行槽函数。注意槽函数是在接收方的线程中执行的。也就是如果你的信号是在另外一个线程中发出的,那么槽函数就不和信号在同一个线程内这也是为啥QT要创造moveToThread()的线程方式,因为如果你在子线程内发出了信号,信号的连接方式选择自动连接(默认就是自动连接)或者队列连接,那么槽函数就会在主线程执行,那你写子线程的意义就为0。
请添加图片描述

3.2.1例子

比如有下面两种信号和槽函数
请添加图片描述
如果一个通过信号连接,一个直接调用
请添加图片描述
那么运行的结果就是
请添加图片描述
因为一个是直接调用,一个依赖QT的事件循环,前者更快。
如果都采用信号的方式

请添加图片描述
结果就会按照发送顺序,如下:
请添加图片描述

3.2.2 使用场景

比如有下面的操作
加载算法数目——加载算法列表——加载算法其他信息
比较容易想到的是通过回调的方式,在子线程中依次调用即可。
但需要传入的参数比较多
如果不想传入,甚至信息都不在内部数据类中存储,只在窗口类中存储,那么可以通过信号的方式。
容易想到的是下图的方式
请添加图片描述

主线程创建线程依次执行,执行完毕后统一写回。但这种方法写回时通过信号(在线程中访问类成员不安全),会发送大量数据。
而且需要等整个流程结束才能操作3个请求返回的数据。如果加载完算法数目就可以做一些别的事,那就会导致效率低。

为了优化,可以考虑下面的方式
请添加图片描述

每加载完一个数据,就发信号写回。等待主线程再次创建新的线程执行新操作。
这种方法能够保证了一定的“并发性”,不用等整个流程结束也可以操作中间变量。
但这种方法流程复杂,需要频繁的创建线程、发送信号。
考虑到上述方法复杂的原因是该流程中后者依赖前者获得的数据,有先后关系,才会有此问题。但QT自己的事件队列本身就具备队列的先进先出特性,所以完全可以省掉一些流程,如下图:
请添加图片描述

子线程执行完之后只管发送信号,不需要管主线程有没有处理完。
因为三个信号是依次发送的,因此也会依次进入QT事件队列、依次被处理。保证了执行的顺序。

3.3 阻塞连接 BlockingQueuedConnection

信号发出后,等待槽函数执行完毕再执行下去,其实就是用在线程中的。子线程中的信号发出后就处于等待状态,槽函数在槽函数所在的线程中执行完毕后,信号所在的子线程才会执行下去。
QT特别强调,信号和槽函数一定不能在同一个线程中。否则会导致程序卡死。
比如主线程中阻塞连接了信号A->同一线程的B函数。
当主线程发出信号A,会阻塞等待B的完成。
而A此时是阻塞的,根本没法执行B,就会导致程序卡死。

3.3.1 源码

请添加图片描述

3.3.2 使用场景

比如有以下流程:
解析安装包——加载升级信息——获取上传的URL——封装、分类、http发送——获取设备安装状态
其中解析安装包、获取上传的URL、获取设备安装状态是需要线程执行的。
比较容易想到的做法:
请添加图片描述

流程比较复杂。观察可以发现,造成流程复杂的原因是下一步依赖上一步主线程的操作。那是不是可以通过阻塞方式,阻塞子线程,直到主线程完成呢?
比如下图
请添加图片描述

子线程处理完之后就发送信号,阻塞在这里,等待主线程完成。

3.4 唯一连接:UniqueConnection

可以和前四个连接方式配合使用,它要求:1.相同信号名;2.相同对象;3.相同槽函数,如果满足这三个条件,你再次connect是不会成功的,connect只有第一次成功,也就是只会调用一次槽函数

3.4.1 源码

请添加图片描述
3.4.2 使用场景
参考下图:
请添加图片描述
A类的蓝色对象a1需要向蓝色区域发送信号更新;
A类的橙色对象a2需要向橙色区域发送信号更新;
那如果不采用唯一连接,就会导致红框区域重复连接,重复响应。

3.5 默认连接方式:AutoConnection

也就是自动连接,根据connect函数就可以知道,connect默认是自动连接,除非你指定连接方式。这个是QT最常用的连接方式,如果信号和槽在同一个线程,那么QT会选用直接连接;如果信号和槽不在同一个线程,那么QT选用队列连接。至于何时选择呢?信号发出以后,也就是emit生效以后,QT会根据是否在同一个线程内自动选择。
3.5.1 源码
请添加图片描述

4.常见信号问题

4.1 还没运行完的线程发送信号

比如下述情况:
打开升级界面
进行升级,创建了子线程去执行
关闭了升级界面,子线程仍在执行
重新打开升级界面,此时子线程执行完毕,发送信号
升级界面收到信号,但还没有开始升级。更新进度,导致出错甚至崩溃

解决方法:

1.通过加判断

加一些防护式的判断

2.延时连接

等使用时再连接,可以一定程度避免风险

3.维护一个唯一ID

维护了一个uiObjectId_子线程发送信号时会带上这个id
主线程收到后做出判断

4.2 一个信号连接了多个类

比如一个父窗口连接了子成员1、子成员2,父窗口发送一个信号,两个子成员都会响应。
再比如类A连接了一个公用信号,A的成员a1创建了线程b1,A的成员a2创建了线程b2,如果b1发送这个公用信号,a1、a2都会响应,可能出错。
上述情况可以通过添加防护式判断,比如将设备的ID作为参数传入,在类内也维护一个ID。收到信号时判断是不是自己的ID即可。

4.3 创建了自己的对象

比如类A的对象a1,需要创建一个自己的子窗口a2并运行
这时候a2内部收到信号,a1也会响应,导致错误。
可以在a2 exec之前将信号解绑,等a2运行完再绑定。

5.总结

1.默认连接:如果信号和槽在同一个线程,那么QT会选用直接连接;如果信号和槽不在同一个线程,那么QT选用队列连接;
2.直接连接:信号发出后槽立刻响应,但是注意信号和槽要在同一个线程中,否则线程不安全
两个都是直连看哪个signal先发出
发出的信号相同看哪个connect先建立,和connect的顺序有关
3.队列连接:信号发出后等待QT的调度,排到他以后再执行槽函数。槽函数是在接收方的线程中执行的
4.阻塞连接:信号发出后,等待槽函数执行完毕再执行下去,信号和槽函数一定不能在同一个线程中,否则会导致程序卡死
5.唯一连接:如果是相同信号相同对象发给相同槽函数,再次connect是不会成功的

  • 17
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值