C++程序卡死、UI界面卡顿问题的原因分析与总结

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

1、概述

2、软件卡死问题

2.1、死循环

2.2、死锁

3、客户端软件的UI界面卡顿问题

3.1、UI线程在频繁地写日志到文件中导致UI线程时不时的卡顿

3.2、从网上拷贝的代码中调用Sleep函数导致UI界面有明显的卡顿

4、总结


VC++常用功能开发汇总专栏文章列表欢迎订阅持续更新...icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程专栏文章列表欢迎订阅持续更新...icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/125529931        本文就平时项目开发过程中遇到的比较典型的软件卡死、UI界面卡顿问题大概地总结一下引发这些问题的原因及相关排查方法给大家提供一些思路和参考。

1、概述

        在日常的项目开发与维护的过程中时常会遇到软件卡死、UI界面卡顿的问题。这类问题对于有经验的开发人员排查起来可能相对容易一些但对于新手或者刚参与项目的人对现有项目代码及业务不熟悉则排查起来可能要困难许多。其实这类问题有一些常用的分析思路和排查方法本文就平时项目中遇到的问题实例结合日常的排查经验对引发这些问题的原因及排查方法做一个大概的总结。

2、软件卡死问题

软件卡死直接表现为进程中的某个或多个线程发生卡死。对于包含UI界面的客户端可能卡死发生在UI主线程就会表现为UI界面无法点击、无法操作的问题。软件也有可能卡死发生在某个或多个业务线程中表现为一些软件业务不能正常运转软件的功能出现问题了。一般线程卡死或堵塞主要是死循环或死锁导致的。下面就来详细地讲述一下死循环和死锁相关内容。

2.1、死循环

       如果代码中出现了死循环会导致死循环所在的函数一直不返回或者一段时间内不返回进而导致函数的主调线程发生卡死。

2.1.1、产生死循环的原因分析

        如果程序执行到死循环代码一般会出现高CPU占用率的情况因为一直在不间断地执行死循环中的代码。死循环可能是for或while中的循环条件有问题也有可能是消息触发的函数调用上的死循环调用这两类场景我们在实际项目中遇到过。

        对于循环条件中的死循环可能是手误将条件中大号或小于号写成了等于号如下所示

for ( int i = 0; i = nChannelNum - 1; i++)
{
    // ......
}

这样上述代码就会无限循环下去。正确的代码应该如下所示

for ( int i = 0; i <= nChannelNum - 1; i++)
{
    // ......
}

       也有可能是循环条件中的变量值是从服务器侧传过来的可能是个很大的异常值比如

for ( int i = 0; i < nChannelNum; i++)
{
    // ......
}

正常情况下nChannelNum是小于10的一个整数值结果实际运行时服务器传过来一个很大的异常值比如nChannelNum = 1023456;这样就会导致一段时间内的死循环。

        至于nChannelNum为什么是个很大的异常值可能是服务器传过来的就是个异常值也有可能是客户端程序底层收到服务器传过来的Json格式在解析时解析错了。

        还有一种由消息触发的函数调用上的死循环我们在实际项目中遇到过几次。比如在UI程序中A函数中调用了接口产生了消息M然后代码进入消息M的处理函数中函数中调用B函数而B函数中又调用了A函数这样就形成了一个闭环即产生了死循环。

2.1.2、死循环的排查

        对于死循环问题如何进行排查呢发生死循环时一般会导致进程的CPU占用较高。此外死循环应该是发生在某个线程中的所以那个线程的CPU占用会比较高我们可以使用工具去查看CPU占用高的线程然后去查看线程的函数调用堆栈通过函数调用堆栈判断死循环发生在哪个函数中。

        我们可以使用Process Explorer工具去分析问题也可以使用Windbg去排查。Process Explorer工具比较简便主要用来查看CPU高的线程查看线程的函数调用堆栈

关于Process Explorer如何使用可以参见我之前的文章
使用Clumsy和Process Explorer定位软件高CPU占用问题icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/120931072        Windbg则是附加到目标进程上除了可以查看线程的函数调用堆栈还可以查看相关变量的内存。使用Windbg如何分析死循环问题我后面会写专门的文章去介绍。

2.2、死锁

        代码中出现了多线程死锁deadlock某个线程调用申请获取锁的某个函数但锁的拥有者一直没有释放导致申请锁代码所在的函数卡住了导致线程调用的函数一直没有返回从而导致线程卡住。

2.2.1、多线程死锁场景及多线程锁的类型

        死锁一般发生在多个线程之间一般会涉及到两个或两个以上的锁。下面我们先大概地讲述一些发生死锁的场景及用于线程间同步的锁的类型。

2.2.1.1、发生死锁的场景说明

         比如当前有两个线程线程1和线程2当前有两个锁锁1和锁2。假设线程1占用了锁1正在申请锁2同时线程2占用了锁2正在申请锁1两线程都占用了各自的锁都在申请对方占用的锁各不相让如下所示

这样就导致了死锁这是个典型的死锁场景。

        还有一个比较典型的场景是线程1和线程2之间发生了死锁导致了线程3的死锁。假设线程2占用了线程3要申请的锁3因为线程1与线程2之间产生了死锁导致线程2一直在占用锁3一直没有释放。而线程3的代码进入了要申请锁3的代码中因为线程2一直在占用锁3不释放这样也导致了线程3的死锁如下所示

2.2.1.2、锁的类型

        此处我们以Windows平台的多线程锁为例来展开。在Windows平台中可以用多个对象来实现多线程间的锁比如临界区对象、事件对象、互斥量对象、信号量对象等。

        这些对象主要分用户态对象和内核态对象其中临界区属于用户态对象事件、互斥量和信号量则属于内核态对象。使用用户态对象的好处是不用在用户态与内核态之间切换在效率上相对高一些所以在Windows平台上用户态的临界区用的比较多一些。使用内核态对象时大部分程序代码都运行在用户态的当操作到这些内核态对象时在底层就需要切换到内核态中完成对应的操作后再返回到用户态代码中。如果代码在用户态和内核态之间频繁的切换则执行效率上会有损伤。

用户态的临界区锁只能用于一个进程中的多个线程间的同步。而事件、互斥量和信号量都属于内核态的对象除了可以用于一个进程中的多个线程的同步还可以跨进程使用。

2.2.2、死锁问题的排查

        死锁问题可以通过运行日志去排查也可以使用调试工具去排查。Windows平台主要使用Windbg如果发生死锁的是用户态的临界区锁则使用Windbg去分析要容易许多Windbg默认是在用户态中的。如果要排查内核态锁引发的死锁则要复杂一些Windbg需要切入到内核态中去分析。

        分析死锁问题首先要确定发生死锁的是那几个线程可以使用Windbg将所有线程的函数调用堆栈打印出来如果线程卡在EnterCriticalSection或者WaitForSingleObject等函数上可能是发生死锁的线程但还要结合业务与代码进行分析。确定发生死锁的线程根据线程的函数调用堆栈分析发生死锁的原因并加以解决。

        关于如何使用Windbg去排查多线程死锁问题可以查看我之前写的文章 

使用Windbg分析多线程临界区死锁问题分享icon-default.png?t=MBR7https://blog.csdn.net/chenlycly/article/details/128532743

3、客户端软件的UI界面卡顿问题

        软件因为死循环或死锁导致软件的某个或多个线程发生卡死线程直接卡住了代码没法继续运行了。而此处讲的UI界面卡顿与软件卡死还是有较大区别的可能是UI线程执行了比较耗时的操作或者是人为地执行了sleep操作。

        UI界面卡顿问题也是比较常见的。比如软件在使用的过程中会时不时的出现卡顿再比如用鼠标点击UI界面会有明显的响应延迟、响应不及时的问题。在某些时段里软件的卡顿是可以理解的也是在合理的范围内的比如在软件刚登陆服务器成功时会到服务器上拉取大量的数据要处理大量的消息和数据软件内部会比较忙碌这个时段的卡顿是正常的当然这种情况下我们也要尽量的进行优化让软件不那么卡顿或者卡顿的时间稍微短一点。

        此处我们主要讨论一些不太合理的UI界面卡顿问题。这类问题我们举两个之前项目中遇到的问题实例简单的说明一下。

3.1、UI线程在频繁地写日志到文件中导致UI线程时不时的卡顿

        UI客户端软件在运行的过程中会频繁地出现短暂性的卡顿问题经分析UI线程在运行过程中频繁地写日志到文件中导致的。写日志到文件中的文件IO操作相对内存操作要慢很多如果频繁地写日志会导致函数不能及时返回导致UI线程会时不时的卡顿。

        解决办法是UI线程将要写的日志放到缓存中新开启一个线程定时检测缓存队列的大小定时或者队列达到上限就在新开启的线程中将日志缓存队列中将日志写到文件中。

3.2、从网上拷贝的代码中调用Sleep函数导致UI界面有明显的卡顿

        某个UI客户端软件测试同事在测试的过程中发现最近几天UI界面操作出现明显不流畅、卡顿问题所有UI界面的操作都有这样的问题这是以前从来没有过的。如果是个别界面在频繁处理数据导致UI界面卡顿是可以理解的但本问题中是所有的UI界面卡顿。

        后来通过历史版本比对法逐一安装多个版本确定问题是从某一天开始有的那应该是前一天提交的代码引发的我们的自动化编译系统每天都会自动编译版本。于是详细查看了前一天的代码修改记录发现是实现定时检测CPU占用率的代码有问题。代码的逻辑是这样的在UI线程中开启了一个定时器在定时器响应函数中调用CPU使用率检测函数而CPU使用率检测函数中调用Sleep函数如下所示

void CalCPURate()
{
    // ...
    
    Sleep(500);
    
    // ...
}

因为计算CPU占用率要找一个参考时间段所以Sleep了一下。

        CPU使用率检测函数在定时器消息中调用的所以Sleep肯定是在UI线程中Sleep的执行Sleep时就会将UI线程挂起不执行了所以就导致了UI界面卡住了。解决办法是新开启一个线程将CPU检测代码放到这个新的线程中去执行。

       此处我们还要再强调一点从网上拷贝的代码块可能不够严谨有很多缺陷或者代码编写时考虑的不够全面我们在使用的时候要认真的走读审核一下要将可能存在的问题消灭在初始之时。比如代码块中有内存泄露如果放置在定时任务中执行结果可能是毁灭性的。再比如本问题中的代码块其中包含了对Sleep函数的调用如果事先认真走读一下代码就知道这样的代码块不能放置到UI线程中执行就不会有此处我们说的这个问题了。

4、总结

        我们在排查问题的过程中要多思考多总结在问题中进步在问题中积累经验在问题中提高要搞清楚问题的来龙去脉哪怕不是自己负责的模块也要大致的了解一下。文中是根据实际遇到的问题总结出来的经验在此详细记录整理一下给大家提供一定的借鉴和参考。
 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++