背景介绍
软件为基于Qt和C++开发的桌面应用软件,主要功能为实现用户设备系统的监控功能和其他业务功能,特点是系统中的设备数量很多,且设备状态数据有两种不同协议的上报。
周末外场传来消息,软件一天内出现了四五次闪退。拿到外场发来的崩溃日志,发现是底层状态解析的地方出现了崩溃。
问题分析
这两个函数位于软件数据层,运行在两个不同的子线程中,分别针对ChangeableLengthFrame和AllFrame两种格式的设备状态数据进行解析。
询问现场后得知,服务端软件的AllFrame数据,由查询后上报改为了每10秒钟上报一次,没有做其他改动。评估这个改动的影响:(1)增加了CPU的压力;(2)增加了AllFrame解析和changeLengthFram解析中的资源竞争的概率。
通过分析现场发回来的info级别日志,发现ChangeableLengthFrame的频率达到了1~3毫秒/帧,这相较于之前的几十毫秒/帧也快了非常多。
问题复现和解决
类似这样的崩溃问题,真实运行环境下通常不容易复现,采取猜测-修改-试用的模式,解决问题的成本过高(时间成本、沟通成本、客户负面印象等),因此需要在仿真环境下解决这个问题。
为此我们设计了一个多线程的demo,模仿这两个解析函数的工作,定位崩溃的原因。主要包括几个类,statepoint是解析的模板类,然后是两个解析的子线程类。主要代码如下:
statepoint类
#ifndef STATEPOINT_H
#define STATEPOINT_H#include <QObject>
#include <QVariant>class StatePoint : public QObject
{Q_OBJECT
public:explicit StatePoint(QObject *parent = nullptr);QString id = "";QVariant value;signals:};#endif // STATEPOINT_H
共享资源类
负责初始化map,解析具体数据与苏,存放解析结果。
//输出化map
void ParamList::initMapIdPoint()
{for(int i=1;i<9999;++i){QString id = QString("%1").arg(i,4,10,QLatin1Char('0'));StatePoint *point = new StatePoint;point->value = i;map_IdPoint.insert(id,point);}
}/// 函数说明-插入statepoint的key和value
/// param key,statepoint的id;value,参数值
void ParamList::insertStatePointIdValue(const QString &key, const QVariant &value)
{QWriteLocker locker(&lockMapStatePointIdValue);map_IdValue.insert(key,value);
}//某个参数的解析
QVariant ParamList::parseVar(StatePoint *point)
{if(!point){return QVariant(0);}QVariant pointValue =0;#if 1int pointProportion = 10;pointValue = point->value;if(pointProportion>1){int size = 3;double scale = 1.0/pointProportion;pointValue = QString::number(scale *pointValue.toDouble(),'f',size);}return pointValue;#endif
}
ChangableFrame线程
//ChangableFrame数据解析
void ChangableFrameParse::process()
{int loopNum = 9999;ParamList *paramList = ParamList::getInstance();for(int num=0;num<loopNum;++num){int randomNumber = QRandomGenerator::global()->bounded(1, 9999);QString id = QString("%1").arg(randomNumber,4,10,QLatin1Char('0'));StatePoint *point = nullptr;{if(paramList->map_IdPoint.contains(id)){point = paramList->map_IdPoint.value(id);}else{continue;}{point->value = paramList->parseVar(point);}}if(point){paramList->insertStatePointIdValue(id,point->value);}// LOG(INFO)<<"ChangableFrameParse:"<<id.toStdString()<<" "
// <<point->value.toString().toStdString();}QThread::usleep(500);
}
AllFrameParse线程
void AllFrameParse::process()
{int loopNum = 9999;ParamList *paramList = ParamList::getInstance();for(int num=0;num<loopNum;++num){int randomNumber = QRandomGenerator::global()->bounded(1, 9999);QString id = QString("%1").arg(randomNumber,4,10,QLatin1Char('0'));StatePoint *point = nullptr;{if(paramList->map_IdPoint.contains(id)){point = paramList->map_IdPoint.value(id);}else{continue;}{point->value = paramList->parseVar(point);}}if(point){paramList->insertStatePointIdValue(point->value);}// LOG(INFO)<<"AllFrameParse:"<<id.toStdString()<<" "
// <<point->value.toString().toStdString();}QThread::usleep(500);
}
反复运行软件,每次5~30分钟不等,demo软件都出现了崩溃的问题,成功复现了外场软件的崩溃问题。接下来的问题就是怎么样把这段代码改稳定了。我们依次做了以下改动:
- 全局操作枷锁。给point->value = paramList->parseVar(point)加锁,重新运行后稳定了很多,但没抗过1个小时又崩溃了。
- 将paramList->insertStatePointIdValue(id,point->value)中的point->value改为一个局部变量QVariant。终于可以稳定运行不会崩溃了。
修改后的代码如下:
void ChangableFrameParse::process()
{int loopNum = 9999;ParamList *paramList = ParamList::getInstance();for(int num=0;num<loopNum;++num){int randomNumber = QRandomGenerator::global()->bounded(1, 9999);QString id = QString("%1").arg(randomNumber,4,10,QLatin1Char('0'));StatePoint *point = nullptr;QVariant value ;{if(paramList->map_IdPoint.contains(id)){point = paramList->map_IdPoint.value(id);}else{continue;}{QMutexLocker locker(¶mList->mutexPoint);point->value = paramList->parseVar(point);value = point->value;}}if(point){paramList->insertStatePointIdValue(id,value);}}QThread::usleep(500);
}
问题复盘
第一个问题比较明显,在多线程中操作全局变量时要加锁,这个大家都知道。主要问题在于很多时候代码是由单线程改为了多线程,这时候要及时处理不同线程间共享资源加锁的问题;另外在复杂的业务代码里,找到关键的位置也很重要。
第二个问题,跟Qt的隐式共享机制有关。QVariant类型是隐式共享的,将全局的point->value传入接口后,接口内并没有像普通c++变量一样执行拷贝操作,仍然是在操作全局的point->value,就为后续的多线程操作埋下了隐患。
总结,软件崩溃的问题很容易把开发人员搞崩溃,一是解决起来很困难,二是不解决是肯定不行的,三是不尽快解决也不行呀。解决崩溃问题的关键,一是要有搜集软件运行信息的手段,二是要和现场的人员及其他相关方多多沟通,发现一些有用的线索。在广泛搜集信息的基础上,做合理假设和有效验证,就可以搞定啦。
补充一点,很多时候软件出现意外的结果,还是因为有些知识点超出了我们现有的认知。通过多读书,多看别人的经验,是成本最低的成长途径。通过自己踩坑和碰壁来提高认知,就算不头破血流也得焦头烂额呀!
最后,感谢您的阅读,希望本文对您有所启发和帮助!