预防Windows应用程序挂起

预防Windows应用程序挂起预防Windows应用程序挂起

受影响的平台

客户端  - Windows 7

服务器端  - Windows Server 2008 R2

描述

挂起 – 从用户角度来看

用户喜欢及时反馈的应用程序。当用户点击一个菜单的时候,他们希望应用程序可以及时反馈,即便应用程序正在进行处理。当他们在自己最喜欢的文字处理工具中保存一篇长文档时,他们希望在磁盘正在旋转的时候仍然能够继续打字。当应用程序不能及时响应用户的输入的时候,用户很容易不耐烦。

程序员可能很容易找到一个应用程序不能及时的对用户的输入进行反馈的合理原因。应用程序可能正忙于重新计算一些数据,或者正在等待磁盘的 I/O 读写完成。但是,通过对用户进行的调查,我们发现用户对于一个没有响应的应用程序等上几秒钟就会变得厌烦,沮丧。 5 秒钟之后,用户将会试着结束这个挂起的应用程序。与崩溃类似,当调用 Win32 应用程序的时候,应用程序挂起是用户中断的最主要原因。

有许多的不同的原因可能引起应用程序的挂起,但并不是所有的情况都会在 UI 上体现出来。但是, UI 没有反映的情况多数是由于应用程序挂起,这种情况通常可以得到系统的支持与恢复。 Windows 自动检测,收集调试信息,并最优化的终止或重新挂起应用程序。除此之外,用户可能不得不重启机器来恢复挂起的应用程序。

挂起 – 从操作系统角度来看

当一个应用程序(或者更准确定的说,一个线程)在桌面上创建一个窗口时,它会执行一个桌面窗口管理工具( Desktop Window Manager , DWM )的隐式的协议来及时的处理窗口消息。 DWM 回发消息(键盘 / 鼠标的输入以及来自于其他窗口或窗口本身的消息)到一个特定线程的( thread-specific )消息队列。该线程将会通过它的消息队列获取并发送这些消息。如果线程没有通过调用 GetMessage() 来为队列服务,则消息将不会被处理,并且窗口会挂起:此时,既不能刷新屏幕也不能接受任何来自于用户的输入。系统会通过向消息队列中的等待消息( pending messages )加入一个计时器来监测到这一状态。如果一条消息 5 秒内没有被获取,那么 DWM 会声明窗口处于挂起状态。您可以使用 IsHungAppWindow() API 来查询特殊窗口的状态。

监测只是第一步。这时,用户仍然不能终止该应用程序 – 点击 X (关闭)按钮可能会引起 WM_CLOSE 消息,它会像其他消息一样被放入消息队列中。 DWM 会无缝的隐藏被挂起的窗口,并在原始窗口的位置上把它替换成一幅图片(并在标题栏上加上“没有响应”)。只要原始窗口的线程没有获取消息, DWM 会同时管理两个窗口,但是只允许用户与替换的窗口进行交互。使用替换的窗口的时候,用户只可以移动,最小化以及——最重要的——关闭没有响应的应用程序,但是不能改变其内部的状态。

整个替换效果如图所示:

DWM 做的最后一件事是:它集成 Windows 错误报告( Windows Error Reporting ),允许用户不仅可以关闭并最优化的重启应用程序,还能够发送有价值的调试信息给 Microsoft 。您可以到 Winqual 网站注册以获得关于您的应用程序的挂起数据。

Windows 7 为之添加了新特性。系统会分析挂起的应用程序,在特定情况下,并给用户一个选择来取消阻止操作并使应用程序重新可用。当前的实现支持对于阻止的 Socket 调用执行取消操作;更多的操作都将会在未来的版本中变成用户可以取消的。

要将您的应用程序与挂起恢复进行集成并从中获得有用的数据,请按以下步骤进行:

确定您的应用程序注册了重启和恢复,以尽量减少挂起给用户带来的痛苦。一个正确注册了的应用程序可以在大多数未保存的数据没有丢失的情况下自动重启。这一操作对挂起和崩溃都有效。 从 Winqual 网站上获得您的挂起或崩溃的应用程序的调试数据以及发生频率的信息。在您的应用处于 Beta 阶段的时候,您可以使用这一信息来改善您的代码。请参看“介绍 Windows 错误报告”。 您可以调用 DisableProcessWindowsGhosting () 来禁用应用程序中的替换特性。但是,这会使用户无法关闭或重启一个挂起的应用程序,并常常会使机器重启。

挂起 – 从开发者的角度来看

操作系统定义了应用程序的挂起为一个超过 5 秒钟没有处理消息 UI 线程。显然 bug 会引起挂起,例如,一个线程等待一个从没有信号的( signaled )事件,以及两个线程互锁的情况。您可以修复这些 bug 而无需做过多的工作。但是,许多挂起并不是十分清晰。事儿, UI 线程是没有获得消息 – 但是它同样在忙于执行其他“重要的”工作并且将在最终完成后回来处理消息。

然而,用户认为这是一个 bug 。设计上应该适应用户的期望。如果由于应用程序的设计导致了应用程序没有反映,则设计就应该进行改变。最后,也同样重要的,没有响应就像是代码的 bug 一样不能被修复;它需要在设计时预先做好工作。试图翻新改进一个应用程序现有的基础代码来使 UI 可以响应,通常需要花费许多。下面的设计准则可能对您有所帮助:

把 UI 的响应放在需求的首位;用户应该始终可以感到可以控制您的应用程序 确保用户可以取消长于 1 秒的操作,或者操作可以在后台完成;如果需要的话,提供适当的进程 UI

队列长时间运行或者阻止操作使其变成后台任务(这需要设计细致的消息系机制,在工作完成的时候可以通知 UI 线程) 使 UI 线程部分的代码尽量简单;尽量多地去除对阻塞 API ( blocking API )的调用 只有当窗口和对话框已准备好并且完全的可以使用的时候,才显示他们。如果对话框需要显示信息,将会消耗许多运算资源,首先应该显示一些一般信息,然后在运行中当数据可以使用时更新这些信息。很好的一个例子就是文件夹属性对话框。它需要显示文件夹的总容量,但文件系统还没有准备好提供相关信息。对话框即时显示容量字段并通过工作线程逐渐更新容量信息:

不幸的是,并没有简单的方法来设计和编写一个及时反馈的应用程序。 Windows 没有提供一种简单的异步框架,该框架考虑到了简单时序的阻塞( blocking )或长时间运行的操作。 下面的部分将介绍一些关于预防挂起的最佳实践,以及常见的易犯的错误。

最佳实践

使 UI 线程简单化

UI 线程的主要职责是获取并发送消息。该线程的任何其他的工作可能会带来窗口挂起的风险。

可以 :

将会引起长时间运行操作的消耗过多或不受控制的算法移动到工作线程( worker threads ) 识别尽量多的阻塞( blocking )方法,并将它们移动到工作线程;任何调用其他 DLL 的方法都应该被视作可疑的 移除工作线程中所有文件 I/O 和网络 API 调用。这些方法可以造成几秒钟甚至几分钟的阻塞。如果您需要在 UI 线程中做任何 I/O 操作,请考虑使用异步 I/O 注意您的 UI 线程正在服务于单线程单元( STA ) COM 服务器;如果您进行了阻塞调用,这些 COM 服务器将会失去响应,直到您再次为消息队列提供服务服务。

不要 :

等待任何内核对象(比如,事件或 Mutex )的时间,超过一个很小的时间;如果您不得不等待,请考虑使用 MsgWaitForMultipleObjects() ,该方法将会在一个新消息到来的时候解除阻止。 使用 AttachThreadInput() 方法将一个线程的窗口消息队列与另一个线程共享。正确的同步访问队列不仅非常困难,并还将阻止 Windows 操作系统监测挂起的窗口。 在您的任何工作线程上使用 TerminateThread() 。 以这种方式终止一个线程将不能释放锁或给事件发信号,并且很容易产生孤立的同步对象。 从您 UI 线程中调用一些“未知的”代码。如果您的应用程序包含可扩展的模块,这种情况很有可能发生;这里不鼓励让第三方的代码遵照您的响应准则 进行任何类型的阻塞广播调用( blocking broadcast call ) ; SendMessage(HWND_BROADCAST) 会使您对每一个正在运行的编写不正确的应用程序毫无办法。

实现异步模式

从 UI 线程中去除长时间运行或阻塞的操作,需要实现一个异步的框架,该框架允许卸载这些操作到工作线程。

可以 :

在您的 UI 线程中使用异步窗口消息 APIs ,特别是用一个对等的且没有阻塞的方法代替 SendMessage ,例如: PostMessage, SendNotifyMessage, or SendMessageCallback 使用后台线程来执行长时间运行或阻塞的任务。使用新的线程池 API 来实现您的工作线程。 为长时间运行的后台任务提供取消支持。对于阻塞 I/O 的操作,使用 I/O 取消,但这只是最后才采用的解决办法;对“正确的”操作进行取消并不容易。 使用 IAsyncResult 模式或事件,来为托管代码实现一个异步的设计。

聪明的使用锁

您的应用程序或 DLL 需要锁来同步访问内部数据结构。使用多种锁来增加并行性并使您的应用程序响应更加及时。但是,使用多种锁还可以增加以不同的顺序获得锁的可能性,以及引起您的线程死锁。如果两个线程都有一个锁并且还都试图获得另一个线程的锁,那他们的操作将会形成一个循环等待,该等待将会阻塞进程向前运行。您只能通过确保应用程序中的所有线程都按照同样的顺序获取所有的锁来避免这个死锁。但是,以“正确的”顺序获得锁并不是一件容易的事情。软件组件可以被构成( be composed ),但是锁获得( lock acquisitions )不能。如果您的代码调用了一些其他的组件,这些组件的锁现在又变成了您的隐含的锁顺序( implicit lock order )的一部分 – 即便这些锁对您来讲是不可见的

由于锁操作包含比通常情况更多的方法——它们作用于关键部分( Critical Sections ), Mutexes , 以及其他传统的锁,这使得事情变得更加复杂了。任何跨线程边界的阻塞调用都有同步属性,这可能引起死锁。调用的线程执行一个包含“获取( acquire )”语义的操作,并不能解除阻塞,直到目标线程“释放”那调用。一少部分 User32 方法(例如 SendMessage ),以及很多阻塞 COM 调用都属于这一类。

更糟糕的是,操作系统有自己的内部的进程特殊( process-specific )的锁,在代码执行的时候会被锁住。当 DLLs 被加载到进程的时候获得了锁,因此被叫做“加载者锁”( loader lock )。 DllMain 方法总是在加载者锁的情况下执行的;如果您在 DllMain (您应该不能)中获取任何锁,您需要让加载者锁成为您锁顺序的一部分。调用特定的 Win32 APIs 也可能获得加载者锁——例如这些方法, LoadLibraryEx , GetModuleHandle 特别是 CoCreateInstance 。

为了更好的说明,请看下面的示例代码。这个方法获取了多个同步对象并隐式的定义了锁顺序,很明显有些地方并不是必要的。在方法的入口处,代码获取了一个关键部分( Critical Section )并且没有释放直到方法结束,因此使得他成为了锁层级结构中的顶节点。代码随后调用了 Win32 方法 LoadIcon() ,它将可能调用操作系统加载器以加载二进制代码。这一操作可能获得加载者锁,它也成为了锁层级结构的一部分(确定 DllMain 方法没有获得 g_cs 锁)。然后代码调用了 SendMessage() ,一个阻塞跨线程的操作,它将不会返回除非 UI 线程进行相应。在此确认 UI 线程从来没有获得 g_cs 。

bool foo::bar (char* buffer)

{

EnterCriticalSection(&g_cs);

// Get 'new data'icon

this.m_Icon= LoadIcon(hInst,MAKEINTRESOURCE(5));

// Let UI thread knowto update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);

this.m_Params= GetParams(buffer);

LeaveCriticalSection(&g_cs); return true;

}

再看这段代码,可以清晰的看出,我们隐式的把 g_cs 放到了我们锁层级结构的顶层,即便我们只想同步访问类的成员变量。

可以 :

设计一个锁层级结构并遵循它。添加所有必要的锁。还有很多像 Mutex 和 CriticalSections 的同步原语( synchronization primitives );它们都需要被包含。如果您需要在 DllMain() 中获得任何锁,请在锁层级结构中包含加载者锁。 与您的依赖在锁协议上达成一致。任何您的应用程序所调用的或者是调用您的应用程序的代码,都需要共享同样的锁层级结构。 锁数据结构而不是方法。把锁获取( lock acquisitions )从方法的入口点移走并只用锁来保护数据访问。如果减少代码操作,则死锁的几率也会降低。 分析您的错误处理中锁获取( lock acquisitions )和释放。当试图从错误状态中恢复时常常忽略锁的层次结构 Often the lock hierarchy if forgotten when trying to recover from an error condition 将嵌套的锁替换为引用计数器——它们不会死锁。列表中被独立锁住的元素和表都是不错的选择。 小心,当等待一个 DLL 的线程处理的时候。假设您的代码可以在有加载者锁的情况下被调用。最好利用引用计数的方式记录您的资源,并用工作线程来做自我清理(然后使用 FreeLibraryAndExitThread 终止清理)。 使用 Wait Chain Traversal API ,如果您想要分析您应用程序的死锁的话。

不要 :

在 DllMain() 中做任何事情,除了非常简单的初始化工作。查看 DllMain 回调方法已获得更多信息。特别是不用调用 LoadLibraryEx 或 CoCreateInstance 。 写您自己的锁原语( primitives )。自定义同步代码会很容易在您的基础代码中产生细微的 bug 。可以使用操作系统同步对象中丰富的选项代替。 在构造函数和析构函数中对全局变量做任何事情,它们是在有加载者锁的情况下被执行的。

小心异常处理

异常处理允许将正常的项目流和错误处理进行分离。由于这种分离,可能很难知道对于异常优先的项目精确的状态,并且异常处理可能会错过处理有效状态的要紧步骤。 对于锁的获得( lock acquisitions )更是如此,它需要在处理程序中被释放掉,以预防未来可能出现的死锁。

下面的示例代码就说明了这个问题。不受控制的对“缓存”变量进行访问,将会偶尔引起访问冲突。访问冲突是由本机的异常处理引起的,但是如果在出现异常的时候关键部分已经被获得的话,并没有很好的方法来确定。(访问冲突可能已经在 EnterCriticalSection 代码中的某处发生了)

BOOL bar (char* buffer)

{

BOOL rc =FALSE;

__try {

EnterCriticalSection(&cs);

while (*buffer++ !='&') ;

rc= GetParams(buffer);

LeaveCriticalSection(&cs);

} __except (EXCEPTION_EXECUTE_HANDLER)

{

return FALSE;

} return rc;

}

可以 :

在任何可能的时候,去除 __try/__except ;不要使用 SetUnhandledExceptionFilter 如果您使用 C++ 进行异常处理,请把您的锁封装到自定义的 auto_ptr 式的模板( custom auto_ptr-like templates )。锁应该在析构函数中被释放。对于本机异常来说,应该在您的 __finally 代码段中进行释放。 小心在本机异常处理中执行的代码;异常可能会泄露很多锁,所以处理程序不应该获取

不要 :

处理本地异常,如果不必要或 Win32 APIs 请求的话。如果您使用本机异常处理来在灾难性错误之后进行报告( reporting )或数据恢复,请考虑使用 Windows 错误报告的默认操作系统机制来代替。 与任何类型的 UI ( user32 )代码一起使用 C++ 异常处理;在回调中被抛出的异常将会穿过系统提供的 C 代码层。这些代码无法识别 C++ 展开语义( unroll semantics )

资源连接

" 介绍 Windows 错误报告 ":  http://msdn.microsoft.com/en-us/isv/bb190483.aspx 异步设计 :  http://msdn.microsoft.com/en-us/library/ms228969.aspx 异步 I/O:  http://msdn.microsoft.com/en-us/library/aa365683(VS.85).aspx AttachThreadInput():  http://msdn.microsoft.com/en-us/library/ms681956(VS.85).aspx auto_ptr:  http://msdn.microsoft.com/en-us/library/ew3fk483.aspx DisableProcessWindowsGhosting ():  http://msdn.microsoft.com/en-us/library/ms648415.aspx DllMain 回调 Function:  http://msdn.microsoft.com/en-us/library/ms682583.aspx 事件 :  http://msdn.microsoft.com/en-us/library/wewwczdw.aspx GetMessage():  http://msdn.microsoft.com/en-us/library/ms644936(VS.85).aspx I/O 取消 :  http://msdn.microsoft.com/en-us/library/aa363789(VS.85).aspx IsHungAppWindow():  http://msdn.microsoft.com/en-us/library/ms633526.aspx 消息队列 :  http://msdn.microsoft.com/en-us/library/ms644928.aspx MsgWaitForMultipleObjects():  http://msdn.microsoft.com/en-us/library/ms684242(VS.85).aspx 新建线程池 API:  http://msdn.microsoft.com/en-us/library/ms686766(VS.85).aspx PostMessage:  http://msdn.microsoft.com/en-us/library/ms644944(VS.85).aspx 重启与恢复 :  http://msdn.microsoft.com/en-us/library/bb525423(VS.85).aspx SendMessageCallback:  http://msdn.microsoft.com/en-us/library/ms644951(VS.85).aspx SendNotifyMessage:  http://msdn.microsoft.com/en-us/library/ms644953(VS.85).aspx 同步对象 :  http://msdn.microsoft.com/en-us/library/ms681924(VS.85).aspx TerminateThread():  http://msdn.microsoft.com/en-us/library/ms686717(VS.85).aspx Windows 错误报告 :  http://msdn.microsoft.com/en-us/library/bb513641(VS.85).aspx Winqual:  https://winqual.microsoft.com/

预防Windows应用程序挂起相关推荐

  1. windows 批处理程序语法

    批处理文件是无格式的文本文件,它包含一条或多条命令.它的文件扩展名为 .bat 或 .cmd.在命令提示下键入批处理文件的名称,或者双击该批处理文件,系统就会调用Cmd.exe按照该文件中各个命令出现 ...

  2. 用vc++穷举windows应用程序密码

    一.引言 随着计算机信息技术的发展,人们越来越重视信息的安全性,信息数据的安全保密已经成为影响计算机发展的一个重要课题.机密文件.商业情报.银行账号.网络密码.科技成果.包括私人信件等等,都成了用户为 ...

  3. 用vc++穷举windows应用程序密码(上)

    2007年10月12日 02:14:00 一.引言 随着计算机信息技术的发展,人们越来越重视信息的安全性,信息数据的安全保密已经成为影响计算机发展的一个重要课题.机密文件.商业情报.银行账号.网络密码 ...

  4. Windows 安装程序进程错误代码和错误信息列表

    错误代码 值 说明 --------------------------------------------------------------------------- ERROR_INSTALL_ ...

  5. Windows自动启动程序的十大藏身之所(转载)

    Windows自动启动程序的十大藏身之所 Windows启动时通常会有一大堆程序自动启动.不要以为管好了"开始→程序→启动"菜单就万事大吉,实际上,在Windows XP/2K中, ...

  6. linux里运行windows,在Linux上运行Windows应用程序

    当前位置:我的异常网» Linux/Unix » 在Linux上运行Windows应用程序 在Linux上运行Windows应用程序 www.myexceptions.net  网友分享于:2015- ...

  7. Visual Studio 2017 - Windows应用程序打包成exe文件(1)- 工具简单总结

    最近有对一个Windows应用程序少许维护和修改.修改之后要发布新的exe安装文件,打包exe文件时,遇到了很头疼的问题,还好最后解决了,记录一下. Visual Studio版本:Visual St ...

  8. Directx11 教程(2) 基本的windows应用程序框架(2)

    Directx11 教程(2) 基本的windows应用程序框架(2) 原文:Directx11 教程(2) 基本的windows应用程序框架(2) 在本教程中,我们把前面一个教程的代码,进行封装.把 ...

  9. 网页设计师的必备选择20 +必需的Windows应用程序

    今天给网页设计师推荐20几个windows下提高工作效率的应用程序,对于设计师来说是必不可少的,希望大家喜欢 1. Skybound Stylizer 虽然我更喜欢使用Firebug时,我发现&quo ...

最新文章

  1. PHP的学习--PHP的引用
  2. WinAPI: SetRect 及初始化矩形的几种办法
  3. 谨慎使用PHP的引用
  4. Ambari中Ranger安装
  5. 网友不同意 | 政协常委袁亚湘院士:不建议大部分孩子学奥数
  6. CPU各寄存器的作用
  7. java 面向对账 抽象_java开发银行支付、对账时证书相关的操作实例
  8. 1345.跳跃游戏IV-LeetCode
  9. 【正点原子MP157连载】第四十四章Linux SPI总线框架-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7
  10. 信息化与计算机基础课课堂融合,高等学校计算机基础课程多元教学系列教材:网页设计与制作...
  11. 单片机移位操作;_crol_ 和_cror_ 的使用
  12. python计算三角形斜边上的中线_直角三角形斜边上的中线的性质及其应用
  13. 解决sysman.mgmt_task_qtable ORA-600 kdsgrp1错误
  14. python程序30行_30行Python代码刷王者荣耀金币,还怕没有金币买英雄?
  15. 史上最全,几百本常用书籍等你来取(面试,java,c,大数据,AI,python,数据结构等)
  16. 计算机的基础单位和四种进制的基本知识
  17. 浅谈angular的作用
  18. 橱柜门板生产下单解决方案
  19. OpenCV 图像/视频 读取 显示
  20. K_A07_005 基于 STM32等单片机驱动 DRV8833 模块按键控制步进电机正反转

热门文章

  1. 面向对象:期待正当年华,与你欣喜相见
  2. 汽车行业全渠道一站式解决方案
  3. SQL语句之OR和AND的混合使用
  4. SSM 整合详细步骤与配置(纯注解)
  5. 视频画质增强最优解:微帧科技视频超高清引擎
  6. vue 微信分享功能
  7. vivo手机使用应用沙盒一键修改蓝牙参数
  8. undertow_进入Undertow Web服务器
  9. mysql 修改字符集为utf8mb4_MySQL数据库修改字符集为utf8mb4
  10. 图片工具网址导航 处理图片免费网站集合