我的应用似乎卡死了。我该怎么办?

定义“卡死”是一个很好的起点。有时又叫"挂起",英文叫"hang"。
当人们说“卡死”时,他们可能意味着各种各样的事情。当我说“卡死”时,我的意思是进程没有进展——进程中的线程要么被阻塞(例如死锁,要么因为来自其他进程的线程而没有被调度),要么正在执行代码(疯狂地)但没有做有用的工作(例如无限循环,或者长时间忙于旋转而没有做有用的工作)。前者不使用CPU,后者使用100%CPU。当UI开发人员说“卡死”时,他可能意味着“UI没有被绘制”,所以本质上它们意味着UI线程不工作——它们进程中的其他线程可能正在做大量的工作,但由于UI没有更新,它看起来是“卡死”。因此,澄清你说“卡死”的意思是第一步,这需要你查看你的进程及其线程。
如果启动任务管理器,它显示每个进程当前使用的CPU数量。如果没有看到CPU列,可以通过单击View\Select Columns并选中“CPU Usage”复选框来添加它。
请注意,如果有多个CPU,则CPU使用率最多为100。假设您有4个CPU,并且您的进程有一个线程正在运行并占用了它所能使用的所有CPU,您将看到进程CPU列是25%—因为您的进程在任何给定时间只能使用一个CPU。
进程的CPU使用率计算为属于该进程的所有线程使用的CPU使用率。线程是在CPU上运行的东西。它们由操作系统调度器调度,后者决定何时在哪个处理器上运行哪个线程。

第一个场景

如果你看到你的进程占用了0个CPU,这就可以解释为什么它挂起(在CPU保持为0的时间段内)–没有线程可以在你的进程中运行!接下来要看的是其他进程的CPU使用情况。如果您看到一个或多个其他进程占用了所有CPU,这意味着您的进程中的线程根本没有机会运行—这是因为其他进程中的线程比您进程中的线程具有更高的优先级(或由于优先级提升而暂时具有更高的优先级)。可能的原因是:
1) 有些线程被标记为低优先级,它们获取了进程中其他线程运行所需的锁。低优先级线程被其他(正常或高)优先级线程从其他进程抢占。当人们错误地使用低优先级线程执行“不重要的工作”或“不需要及时完成的工作”时,就会发生这种情况,而没有意识到几乎不可能避免对这些线程进行锁定。我听说过很多人说“但我没有锁定低优先级线程”,这不是一个有效的论据,因为你调用的API或你使用的操作系统服务可以通过锁来运行你的代码——在本机NT堆上分配可以获得锁;即使触发页面错误也会导致锁(这不是应用程序开发人员可以在代码中控制的东西)。
2) 您的进程中的线程具有正常的优先级,但其他进程具有高优先级线程-这应该相对容易诊断(除非某些进程只是坏人,否则这种情况很少发生)–您可以查看这些进程正在做什么(再次查看其线程的调用堆栈是一个很好的开始)。

第二个场景

上面挂起的场景是您的进程占用了0个CPU,而CPU被同一台机器上的其他进程占用。下一个场景是您的进程占用了0个CPU,而这个CPU几乎不被其他进程使用。
正如一位同事正确地指出的,这很可能是因为您有一个死锁。通常,调试死锁是相对简单的——您可以查看线程在等待什么,并找出其他线程持有锁。如果您使用Windows调试器包,则会有内置的调试器扩展DLL来帮助您这样做,像!locks等命令。如果您正在调试托管应用程序,SoS调试器扩展有一些命令可以帮助您–!SyncBlk,向您显示托管锁(对于clr2.0和以后的版本,还有 !Dumpheap –thinlock用于使用ThinLocks而不是SyncBlk锁定的对象)。

另一种可能是进程没有执行任何与CPU相关的活动。一个常见的活动是IO—例如,如果进程大量分页,则CPU使用率几乎为0,但它似乎挂起,因为它所需的内存从磁盘加载,速度非常慢。进程监视器是一个非常有用的工具,它可以显示进程正在做什么。昨天我机器上的一个程序周期性地暂停——非常烦人。所以我使用了进程监视器,它告诉我这个程序会定期检查我是否在另一个程序中登录到我的帐户,因为我没有,它会让我登录,做一些事情然后注销我。挂起是因为等待网络IO。所以为了让它高兴,我自己登录,然后恼人的周期性挂起消失了。

第三个场景

现在,如果你的进程确实占用了CPU,那么它也可能会挂起——正如我上次提到的,这意味着不同的人有不同的事情。如果你有一个用户界面应用程序,这可能意味着用户界面没有被绘制;如果你有一个服务器应用程序,这可能意味着你的应用程序没有处理请求。所以你必须定义挂起对你意味着什么。我将以服务器应用程序不处理请求为例。通常服务器应用程序运行在专用计算机上。因此,让我们假设这里是这样的——在一台机器上运行一台服务器,服务器可能由多个进程组成。您可以通过吞吐量来衡量服务器性能。一种情况是CPU使用率很高(甚至比平时更高),但吞吐量比平时低。
最简单的情况是无限循环,非常容易调试。你用调试器中断几次,发现一个线程占用了所有的CPU,而这个线程无法退出某个函数——于是你的无限循环就出现了。如果这个进程很明显的使用了你的CPU。如果您有多个cpu,而其他进程也在使用cpu,则会变得更加复杂。但由于它是一个无限循环,所以如果你不干涉它,它就会一直在执行,所以一旦发生,你就可以随时进行调查。

如果绞刑只是偶尔重演,而当绞刑只持续一小会儿,它就变得很难了。是时候拿出一个CPU剖析器了。,processexplorer是一个非常有用的工具,可以帮助您开始使用它。它向您显示哪些进程是“活动的”—意味着它正在使用CPU。就我个人而言,我从收集适当的性能计数器开始,部分原因是微软产品组中几乎所有的测试团队都有某种自动化的测试过程来收集性能计数器,因此使用它们很容易。而且由于开销低,您可以长时间收集它们,所以您有一个直方图。

以下是我通常请求的计数器(在[]中的注释):

Processor\% Processor Time for _Total and all processors[这样我就知道我要看的CPU使用率是什么样的,以及是否有比其他处理器使用更多的特殊处理器]

Process\% Processor Time for all processes or less the ones that you already know can not be the problem

Thread\% Processor Time for all processes or less the ones that you already know can not be the problem[计数器将告诉您哪些线程正在使用CPU,以便您知道要查看哪些线程]

由于我通常查看与GC相关的问题,所以我请求.NETCLR内存下的所有计数器,所有托管进程的.NET CLR内存计数器(或更少的进程)可能不是问题所在。如果你在看其他东西,你应该添加适当的计数器,例如,ASP.NET使用的应用的计数器。

如果您知道流程的活动类型,可以为它们添加适当的计数器。对于我来说,我经常请求内存相关计数器,例如:

Memory\% Committed Bytes In Use

Memory\Available Bytes

Memory\Pages/sec

Process\Private Bytes for processes I am interested in

在这一点上,我可以查看结果并将注意力集中在有趣的部分上——例如,当CPU通常很高的时候。我将了解哪些线程在哪些进程中消耗CPU,以及我感兴趣的方面(通常是GC和其他内存活动)。然后我可以请求关于这些进程/线程的更详细的数据。例如,我可以要求用户使用采样分析器,这样我就可以看到我感兴趣的部分正在执行哪些函数(以及其他信息—这取决于您所使用的分析器的功能)。

有些人更喜欢在进程挂起时进行内存转储,有时这不一定有效(当它工作时就很好),因为如果挂起与线程的计时/调度方式有关,那么当您中断线程以获取内存转储时,线程的行为很容易不同,因此挂起可能不再重新出现。如果你确实有一个挂起的连续转储,那么你可以使用!runaway命令来查看哪些线程正在消耗CPU。一个转储对于调试挂起几乎没有用处,因为它只在某个时间点提供进程行为的信息。

 

posted on 2020-09-04 07:57  活着的虫子  阅读(591)  评论(0编辑  收藏  举报

导航