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

目录

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界面卡顿的问题。这类问题,对于有经验的开发人员,排查起来可能相对容易一些;但对于新手或者刚参与项目的人(对现有项目代码及业务不熟悉),则排查起来可能要困难许多。其实这类问题有一些常用的分析思路和排查方法,本文就平时项目中遇到的问题实例,结合日常的排查经验,对引发这些问题的原因及排查方法做一个大概的总结。

扫描二维码关注公众号,回复: 14548908 查看本文章

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、总结

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

猜你喜欢

转载自blog.csdn.net/chenlycly/article/details/128703097