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也不会在出现前面的崩溃问题。