关于QT中信号与槽的参数传递,作用域的问题,崩溃

2022-01-08 更新

关于之前提到的信号和槽传递QImage指向同一个地址的问题,参考Stack Overflow的回答,可能是因为QT的隐性拷贝机制,导致传递的QImage参数是隐性拷贝的,所以指向同一片内存。所以比较安全的方法,是在槽函数中接到图像后,立刻用.copy()方法进行一次深度拷贝。事实证明使用次方法确实可以有效降低崩溃的概率,但是也存在队列堆积,导致槽函数开始响应一个消息的时候该片内存已经被释放的可能性。结合以下的工作经历,再写一些自己的思考吧。

在调用相机的程序中,出现了新的问题。在调用海康的SDK进行相机图像读取的时候,需要预先分配好一片连续的缓存区域。由于相机型号基本固定,而且图像不压缩,可以预先知道图像的精确尺寸,并且预先分配好内存,而不是每次采集图像之前再申请。原因是因为之前提到过的,新申请的内存一旦释放,其它通过信号和槽再次调用该内存的线程会立刻崩溃,而成员变量在这个类被析构之前一直都是安全的,可以在一定程度上避免崩溃。

#define m_buffer_size 28829184   //2900w像素
...
class ArrayCapture : public CaptureBase
{
    Q_OBJECT
public:
    bool m_pGrabBuf_valid = false;
    uchar frame_buffer[m_buffer_size] = {0};    //预先分配的缓存
    unsigned int m_Buffer_Size;                 // 图像缓存大小
    unsigned int m_payload_Size;                 //图像数据大小
    unsigned int nWidth;
    unsigned int nHeight;
....

这种方法可以正常工作,而且程序运行不容易出现问题。但是存在两个问题

1. 在编译的时候,QT经常提示“编译器空间不足”。即使不提示的时候,编译程序会猛然占用大量的内存(编译用的电脑128G内存,会在几秒钟内吃满,然后卡上一分钟,编译完成)。重装了编译器和QT环境后有所好转,但是还是会偶尔出现这个问题。观察任务管理器,每次进行编译的时候,jom会生成一大堆编译线程,应该是采用了多线程编译的方法,在这个编译过程中,这个巨大的buffer造成了编译程序也白白占用了大量的内存空间。

 

2. 如果申请的空间的内存空间比较大,会直接无法编译通过,甚至直接让QT挂掉。这里要提一下C++的机制。如参考文档所说,一个进程的堆空间是共用的,各个线程新申请的内存都在这个堆里面;栈空间是各个线程“自有”的,而且有一定的大小限制,不能预先分配一个巨大的空间进去。这里打引号是因为,通过直接暴露内存指针,让别的线程也访问自己的栈。一般来说,默认堆栈大小为 8388608(8GB),堆栈最小为 16384(16KB), 单位为字节。所以,需要大量内存空间存储数据的时候,需要临时到堆上申请使用。

参考文档 (37条消息) 线程堆栈大小的使用介绍_nancy的专栏-CSDN博客_线程堆栈大小

​​​​​​(37条消息) 【C/C++笔记】之多线程中的堆栈问题_醉逍遥_翔的博客-CSDN博客_多线程堆栈

第二种方法,是在程序初始化的时候,只生成一个指针。具体的内存空间,在该类的初始化过程中再new出来。具体方法如下:

头文件中:

class ArrayCapture : public CaptureBase
{
    Q_OBJECT
public:
    unsigned char*  m_pGrabBuf = nullptr;            // ch:用于从驱动获取图像的缓存
...

cpp文件中:
...

int ArrayCapture::init_parameters(void *param)
{
    qDebug() << "[ArrayCapture] Array初始化连接参数";
    int nRet;
    Array_Config *config = (Array_Config *)param;
...


        if (m_pGrabBuf != nullptr)  //如果图像缓存不为空要先释放
            delete m_pGrabBuf;
        m_pGrabBuf_valid = false;  //记录内容是否有效
        if (m_payload_Size < m_buffer_size)
        {
            m_Buffer_Size = m_buffer_size;
        } else  m_Buffer_Size = m_payload_Size;

        m_pGrabBuf =  new uchar[m_Buffer_Size];   // 申请内存
...

一开始,测试运行是没有问题的。问题出在后期,由于性能问题,该类被放到QThread中运行,就开始经常性出问题。思考再三,感觉还是线程间调用资源引起的。通QThread::currentThreadId()进行观察,才想起来,这个初始化函数(类的共有成员),是在主线程中直接调用的,实际上是在主线程中运行,这块内存也就是在主线程中申请的。而采集图像,是在另一个线程中运行的,在这个线程中调用memset对缓存空间进行清空的时候,就会引起程序崩溃,可能是在这里触发了什么保护机制。

(未完待续)

--------------分割线,以下为2021-03-07 06:19:41更新的旧文章-----------------------------

记录一个现象:在QTcreator中,Debug信息出现C:\Program Files (x86)\sogoupinyin\Components\这个莫名其妙的信息的时候,百分之百是因为程序里引用了某个野指针 

很奇怪为什么是搜狗拼音...如果没安装搜狗拼音输入法,这里会出现什么,微软输入法吗...?

近期在一个项目中用了多线程技术。结构其实很简单:

主线程A主要负责过程控制和界面维护。每个传感器又一个子线程B负责维护,数据由子线程进行采集和处理,完成处理后,将数据结果和图片通过信号传递回主线程,并在UI上显示。

最开始,是通过回调函数的方式工作。主线程初始化子线程的时候,将界面上控件C的指针直接传递给子线程,由子线程调用控件的方法进行显示。这个方法在我的机器上一直工作正常。但是将代码带到现场工控机上工作时,只要一显示就有可能发生崩溃,并且随着使用时间增加,崩溃概率越来越大。

在排除了硬件问题后,怀疑是因为线程之间直接调用指针的问题引起的。

后来索性花了一个晚上,将所有的函数都改为通过信号和槽传递消息。这种方法被证明比第一种更靠谱,在编程主机和现场工控机都能正常工作。

但是在后来的工作中,程序内容越来越多,又开始出现崩溃问题,出现的提示依旧是搜狗拼音输入法。再次熬夜debug,发现崩溃只发生在一个子线程B更新控件C上的图片时出现。

吊鬼的是,明明C控件已经收到了这种图片,并且debug显示图片的大小都是正正常的!但是后续将图片转化为qpixmap进行显示,就会提示搜狗拼音!

于是晚上又挠掉了一地头发,发现无论是通过最早的直接调用,或者通过QT的信号机制(connect的时候已经加上了 Qt::QueuedConnection标记位了),程序传递的参数都是一个跟发送(调用)者有关的一片内存。

怀疑是当这个参数被调用的时候,如果发送(调用)者由于某种原因被释放掉了,那么这个内存就变成了一个莫名的地方,调用它的指针就会变成野指针!

于是来看debug结果

[B的成员]处理结束Time used: 2117 ms

[B的成员]生成Mat图片 数据指针= 0x21e88aff080

[B]callbackSetImage  this thread= 0x6544  生成并发送qimage图片  数据指针= 0x21efeb87ad0 

[C]get image, this thread= 0x447c , image size= QSize(3200, 5074) sn= "" sender= LJXCapture(0x21ef603a140) image pointer= 0x21efeb87ad0

C:\Program Files (x86)\sogoupinyin\Components\

05:38:02: 程序异常结束。

上面可见,B中通过CV::MAT生成了一个QIMAGE,并且通过信号发送刚出去。B和C不在一个线程,但是C收到的QIMAGE的地址和B发送的是一样的。也就是说,并没有通过某种机制,来产生一个新的QIMAGE复制体来传递给C,这个和我原来预想的情况并不一样!

恰恰在B发送这个信号的函数中,发送图像的信号是函数最后发出的。发送的信号中,这个图像也是一个局部变量!刚刚发送出去,控件C接收到这个指针的时候,这个地址还没有被释放掉。但是当图片要进行转换显示的时候,好死不死的B的函数返回了,这个局部变量变成了孤魂野鬼一样的野指针...

妈的,说好的线程安全呢,说好的消息机制呢

手动在B线程发送信号的语句以后,加上了Sleep(1000)给这个函数强行续命,结果果然就不崩了!但是Sleep还是会影响性能,不能作为长久的解决方案...

最终的解决方案,在B初始化的时候,申明一个全局变量,用这个全局变量作为发送数据的载体。这时即使删掉Sleep也不会在出现前面的崩溃问题。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值