更新记录(仅首版,2026年08月22日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176687)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《“无响应”的真相——Windows 如何判定应用卡死,以及不会卡死的设计》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-app-not-responding-hang-mechanism/
- DOI(已登记存档)
- 10.5281/zenodo.22176687
- DOI(上次登记版本)
- 10.5281/zenodo.22176688
“处理进行到一半,画面变白,标题栏上出现‘无响应’。”“用户说‘偶尔会卡死’,可在开发机上复现不出来。”——这是 Windows 业务应用中常见的困扰。
首先要明确的是,显示“无响应”的不是应用自己,而是 Windows。Windows 检测到窗口的消息处理停住了,就把原来的画面换成一个替代窗口。追查原因的关键在于,负责这个窗口的 UI 线程正在做什么,以至于处理不了下一条消息。12
本文面向在 Windows 上开发业务应用的开发者,以及接到卡死投诉的信息系统负责人,按操作系统的判定 → 消息循环 → 按原因排查 → 不会卡死的设计 → 调查步骤的顺序梳理。如果你要调查的应用此刻正卡着,请先读第 7 章。
1. 先说结论:不是“消掉显示”,而是“腾空 UI 线程”
把“无响应”的机制、修法和查法按下面的方式划分,思路会更清晰。
| 想了解的事、遇到的困扰 | 首先要掌握的要点 | 详细说明 |
|---|---|---|
| Windows 依据什么判定为无响应 | 看的是窗口及其所属 GUI 线程的消息处理,不是对整个进程作判定 | 第 2 章 |
| 为什么按钮和重绘都停住了 | 因为 UI 线程从事件处理或等待中回不来,取不出下一条消息 | 第 3、4 章 |
| 不想让耗时处理把画面卡死 | 把 CPU 计算和只有同步版本的 API 分离到工作线程,I/O 使用异步 API | 第 5 章 |
| 能不能用 DoEvents 或设置只把显示规避掉 | 会招来重入缺陷,或者只是把显示藏起来,不能当作根本对策 | 第 6 章 |
| 想查清偶尔卡死的原因 | 在结束进程或重启之前,抓取卡死那一刻的转储 | 第 7 章 |
对策的原则是,不要在 UI 线程上长时间等待,也不要做繁重的计算。UI 线程负责输入、绘制、进度显示和接受取消,与耗时的工作分离开。3
另外,显示出“无响应”之前的时间,和操作起来足够顺畅的响应时间是两回事。并不是不到 5 秒就可以。这个区别在 4.5 说明。
2. “无响应”是操作系统的判定:5 秒规则与替代画面
2.1 判定的单位不是整个进程,而是窗口及其所属线程
按微软对 IsHungAppWindow 的说明,满足以下条件的窗口会被视为无响应。1
| 条件 | 内容 |
|---|---|
| 不在等待输入 | 不处于等待输入的状态 |
| 不在启动处理中 | 不处于应用的启动处理过程中 |
| 没有取出消息 | 在 5 秒的内部超时时间内没有调用 PeekMessage |
操作系统看的不是应用在计算什么,而是消息循环还在不在转。消息循环是指依次取出输入和重绘请求并加以处理的机制。具体动作在第 3 章说明。
同一份资料还明确写着,5 秒这个值将来有可能变化。它是无响应判定的内部超时,而不是“UI 可以停住 5 秒”的设计基准。1
判定不是按进程进行的。在拥有多条 UI 线程的应用中,即使某个窗口卡死了,由另一条线程负责的窗口仍可能在正常工作。调查时也不能只看进程,还必须确定卡死窗口所属的那条线程。
2.2 发白模糊的画面是“幽灵窗口”
顶层窗口被判定为无响应后,Windows 会隐藏原窗口,换成具有相同 Z 顺序、位置、大小和外观的幽灵窗口。标题栏上的“(无响应)”,以及在 Aero 主题下发白模糊的外观,都来自这个替代画面,而不是卡死的应用本身。2
| 画面的状态 | 用户看到的情况 |
|---|---|
| 应用原本的窗口 | 处理不了消息,对点击和重绘都没有反应 |
| 操作系统准备的幽灵窗口 | 只能做移动、调整大小、最小化、关闭这类有限的操作 |
即使能拖动这个替代窗口,也不代表应用的内部重新跑起来了。只是操作系统代为承担了最低限度的操作。24
遇到“这个提示出现得太早了”的反馈时,也要先把它当作 UI 线程长时间回不到消息处理的问题来看待。比起压制显示,先查清停住的处理才是正事。
2.3 调试时不出现提示,不等于没有卡死
附加了调试器时,操作系统不会创建幽灵窗口。因此,即便出现“调试运行时不出现,正常运行时变成无响应”的差异,也可能只是 UI 线程同样被堵住,仅仅显示不同而已。2
也有按进程禁用幽灵窗口替换的 API,但它并不是修复卡死本身的 API。6.2 会按用途分别说明。
3. 画面为什么会停住:UI 线程与消息循环
3.1 输入和重绘由同一条线程处理
Windows 的 GUI 应用是事件驱动的。它接收鼠标、键盘、重绘请求、定时器等消息,并执行与各条消息相应的处理。每条创建了窗口的线程都持有一个消息队列,并转动下面这样的循环。5
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // 调用窗口过程
}
GetMessage 从队列中取出消息,DispatchMessage 调用窗口的窗口过程。窗口过程是针对消息执行相应处理的函数。按钮的点击处理、重绘,以及 WinForms 和 WPF 的事件处理程序,也都连着 UI 线程上的这套消息处理。6
flowchart TB
accTitle: 消息循环的基本结构
accDescr: 操作系统把鼠标和键盘等输入放入线程的消息队列,UI 线程的循环用 GetMessage 取出并由 DispatchMessage 调用窗口过程,处理结束后回到循环开头
os["操作系统(输入、重绘请求、定时器)"] --> q["线程的消息队列"]
q --> gm["用 GetMessage 取出"]
gm --> dm["DispatchMessage"]
dm --> wp["由窗口过程处理"]
wp --> gm
图1:取出消息,由窗口过程处理,再回到循环。正是这个转动支撑着输入与绘制。
那么,如果在点击处理程序里调用一个耗时很久的处理,会发生什么?在那个处理返回之前,UI 线程取不出下一条消息。之后到达的点击和重绘请求都处理不了。这就是“画面卡死”的基本结构。
3.2 PostMessage 与 SendMessage 的返回时机不同
消息的投递有两条路径:一条是放进队列,另一条是等待处理完成。57
| API | 行为 | 调用方返回的时机 |
|---|---|---|
PostMessage |
把消息放入队列,由接收方取出并处理 | 放入队列后即返回,不等待处理完成 |
SendMessage |
把消息送到窗口过程,让它执行处理 | 在窗口过程处理完之前不返回 |
尤其是向另一条线程的窗口发送 SendMessage 时,在接收方能够处理该消息之前,发送方也一直被迫等待。这个差异直接导致 4.3 的死锁。7
4. 按原因区分:堵住 UI 线程的 5 种模式
表面上同样是“无响应”,但堵住线程的东西各不相同。先把候选分开,最终用第 7 章的转储或跟踪来确认。
| 候选原因 | UI 线程上正在发生的事 | 常见的表现 |
|---|---|---|
| 同步 I/O 与网络调用 | 等待文件、数据库、Web API 等完成 | 开发机上很快,却在生产或特定环境下卡死 |
| 等待锁 | 取不到工作线程持有的锁,只能等待 | 取决于处理的交叠方式,偶尔才卡死 |
跨线程的 SendMessage |
等待另一条线程处理消息 | 被相互等待或广播牵连进去 |
| 对 COM STA 的调用 | 等待被调用方 STA 处理消息 | UI 线程的停滞会波及其他线程的 COM 调用 |
| 短处理的累积 | 单次虽短,但连续执行期间回不到循环 | 只有在数量变多时操作才变卡 |
4.1 同步 I/O 与网络调用
最常遇到的,是在按钮的点击处理程序里以同步方式读写大文件、执行数据库查询、调用 Web API 或访问网络驱动器的模式。
即使在开发机上零点几秒就结束,遇到生产环境的网络延迟或文件服务器状态不佳,也可能变成几十秒的等待。网络驱动器在连接中断时超时很长,会让症状更严重。“在开发机上很快”并不能成为可以在 UI 线程上等待的理由。
4.2 UI 线程等待工作线程握住的锁
保护共享数据的锁也会成为界面停住的原因。如果工作线程长时间持有锁,而 UI 线程也要去取同一把锁,UI 线程就会一直等到取得为止。
即使把工作移到了工作线程,只要结构上仍由 UI 线程等待该工作结束或等待锁释放,画面就腾不出来。关于锁的纪律,多线程实务系列有详细讨论。
4.3 用 SendMessage 互相等待对方完成
最典型的死锁组合是:UI 线程等待工作线程结束,而这条工作线程又向 UI 发送 SendMessage 并等待。UI 正在等待,处理不了消息;工作线程也从 SendMessage 回不来,于是两边都无法前进。75
sequenceDiagram
accTitle: 跨线程 SendMessage 导致的死锁
accDescr: UI 线程因等待工作线程的结果而阻塞,此时工作线程向 UI 线程的窗口发送 SendMessage,两者互相等待对方完成,形成死锁
participant U as UI 线程
participant W as 工作线程
U->>U: 等待工作线程完成(阻塞)
W->>U: SendMessage(处理完成前不返回)
Note over U: 处理不了消息(正在等待)
Note over W: 无法从 SendMessage 返回
Note over U,W: 互相等待,形成死锁
图2:只是分离到工作线程还不够。UI 与工作线程互相等待对方处理完成时,就会死锁。
向 HWND_BROADCAST 发送同样需要注意。只要有一个窗口不响应,发送方就会被牵连进去。在不能一直等下去的场合,可以考虑带超时的 SendMessageTimeout,或者不等待处理完成的 PostMessage。8
4.4 COM 的 STA 停住,其他线程也被牵连
其他线程对 STA 对象的调用是以窗口消息的形式投递的。因此,一旦 UI 线程(STA)被堵住,指向它的 COM 调用也会阻塞。这种结构下,不只是界面,连调用方都会进入等待状态。
关于套间与消息处理的关系,COM STA/MTA 的文章中有讲解。
4.5 把“单次只是一瞬”重复 100 次
单次 50 毫秒的同步处理,连续做 100 次就是 5 秒。不要只测量单个函数就判断它很短,而要按UI 线程回到消息循环之前的累计时间来考虑。
用户体感上的“发涩、迟钝”从 100 毫秒左右就开始了。设计时不要把无响应判定的 5 秒当作目标,而应认为允许堵住 UI 线程的时间是毫秒量级的。
5. 不会卡死的设计:把工作的执行与画面的更新分开
5.1 区分计算、同步 API 与异步 I/O
对策的原则是,把耗时的工作赶出 UI 线程。不过,并不是所有工作都用同一种方式搬走。
| 工作的种类 | C# 中的基本处理方式 | UI 线程的职责 |
|---|---|---|
| CPU 负载高的计算 | 用 Task.Run 移到工作线程 |
异步等待完成,并显示结果 |
| 只有同步 API 的处理 | 分离到工作线程 | 不要因同步等待完成而堵住界面 |
| 有异步 API 的 I/O | 对 GetStringAsync 等使用 await |
等待 I/O 期间回到消息处理 |
| 输入、绘制、进度、取消 | 由 UI 线程负责 | 不要混入耗时计算或同步 I/O |
异步 I/O 不需要仅仅为了等待就占用一条线程。原本的代码示例也是这样区分使用的:计算用 Task.Run,HTTP 通信用原生的异步 API。3
5.2 C# 中用 async/await 回到 UI 线程再更新
下面是在 WinForms 的点击处理程序中,把繁重工作与界面更新分开的例子。在这个从 UI 事件处理程序启动、并保持 UI 执行上下文的例子中,await 的延续会回到 UI 线程,因此可以更新控件。3
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU 负载高的处理、只有同步版本的 API 用 Task.Run 移到工作线程
var result = await Task.Run(() => HeavyCalculation(input));
// I/O 使用原生异步的 API(也不消耗线程)
var data = await httpClient.GetStringAsync(url);
// await 之后已经回到 UI 线程,可以直接操作控件
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// 异常从 async void 处理程序漏出会让应用崩溃,在这里接住
MessageBox.Show($"处理失败:{ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
需要读懂的要点有以下 4 个。
| 位置 | 意图 |
|---|---|
用 await 等待完成 |
产生等待时把 UI 线程交还给消息循环 |
在 await 之后更新画面 |
不从工作线程直接操作控件 |
| 执行期间禁用按钮 | 防止同一处理被反复启动 |
catch 与 finally |
不让异常从 async void 处理程序漏出,并恢复按钮状态 |
这只是展示职责划分的代码示例,HeavyCalculation 等的实现、窗体关闭时的处理,以及进度和取消处理都省略了。长时间处理所需的进度显示和中断,要按 5.4 的方式另行加入。
WinForms 的控件和 WPF 的元素要从创建它们的线程上操作。从工作线程直接操作会导致异常或未定义行为。需要显式切回 UI 线程时,WinForms 用 Control.Invoke / BeginInvoke,WPF 用 Dispatcher.InvokeAsync。3
5.3 Win32 中用 PostMessage 通知完成
在原生 Win32 中,职责划分也是一样的。把工作交给工作线程,完成后用 PostMessage 发送自定义的完成消息,由 UI 线程的窗口过程更新画面。PostMessage 放入队列后即返回,因此工作线程不会等待 UI 处理完成。7
flowchart TB
accTitle: 不会卡死的应用的职责划分
accDescr: UI 线程只负责接受输入、显示进度和接受取消,繁重处理由工作线程执行,完成后通过 PostMessage 或 await 的延续返回给 UI 线程
ui["UI 线程:输入、进度、取消"] -->|"交付工作"| w["工作线程:繁重处理"]
w -->|"PostMessage / await 的延续"| ui
ui -.-> ng["禁止在 UI 线程上做同步 I/O 或长时间计算"]
图3:繁重计算和同步处理交给工作线程,画面的更新交回 UI 线程。异步 I/O 按 5.1 所述,使用异步 API 等待。
工作线程之间的等待汇合本身,按条件变量的文章中讲过的纪律来写。UI 线程不要反过来同步等待工作线程,这一点同样重要。
5.4 从一开始就设计进度与取消
即使画面没有卡死,如果长时间什么都不变,用户也判断不出它是否在运行。因此,要把进度显示和接受取消也作为处理的一部分来设计。
| 通知的方向 | 使用的机制 | 作用 |
|---|---|---|
| 工作线程 → UI | IProgress<T> |
把进展情况传达到画面 |
| UI → 工作线程 | CancellationToken |
传达中止的请求 |
工作线程在合适的断点处中断,并做好善后。有了进度和中止的手段,就能减少用户不得不依赖强制结束的场面。因为强制结束有时会导致数据损坏。
6. 看似规避手段的两种方法及其局限
6.1 DoEvents 会把别的事件引进处理的中途
在繁重处理期间插入 Application.DoEvents() 或 PeekMessage 循环,确实可以处理消息、规避掉无响应显示。但这样一来,在原处理还没结束的中途,别的事件处理程序就会被执行。这就是重入。
例如,在加工数据的中途调用 DoEvents,积压的按钮二次点击被处理。那个处理程序改写了同一份数据之后,原处理才恢复执行。按这个顺序,原处理所假定的状态早已不复存在。
重入的不只有按钮。关闭窗体的操作和定时器也会挤进来。正在处理的数据被破坏、触碰已关闭的窗体而抛出异常,这类缺陷依赖时机、难以复现,往往比无响应更难调查。
原则是分离工作,而不是用手去转动消息泵。手动的消息处理只应限定在模态进度对话框这类受限的结构之中。一般的长时间处理,使用第 5 章的分离和重入防护。
6.2 禁用幽灵窗口也不会让操作恢复
DisableProcessWindowsGhosting 是禁用调用方进程的幽灵窗口替换的 API。禁用状态在该进程的整个生存期内持续。4
它是为自助终端等特殊用途准备的,用于避免“操作系统弹出一个可以操作的替代窗口”。消息处理已经停住这个事实并没有改变,而且用户还会失去借助幽灵窗口移动或关闭的手段。它不是一般应用用来应对无响应的手段。
| 方法 | 会改变的事 | 遗留的问题 |
|---|---|---|
用 DoEvents 等手动转动消息泵 |
在处理中途也会处理别的消息 | 任意事件都会重入,状态可能被破坏 |
DisableProcessWindowsGhosting |
停止操作系统换成替代窗口的行为 | UI 线程依然被堵住 |
| 把耗时的工作从 UI 分离出去 | 让 UI 线程能够回到输入与绘制 | 还要一并设计进度、取消和重入防护 |
7. 调查步骤:关掉之前,先把卡死的瞬间留下来
7.1 首先抓取转储
调查“偶尔卡死”时最有价值的,是卡死那一刻的线程状态。一旦结束进程或重启,这个状态就丢失了。
在任务管理器的“详细信息”选项卡中右键点击目标进程,选择“创建转储文件”。这样可以取得包含全部线程堆栈的完整转储。也要把“卡死后先抓转储再关掉”这条步骤共享给接收工单的一方。
关于采集机制的搭建,请参见崩溃转储采集的文章。
7.2 查看卡死窗口所属的线程
用 WinDbg 打开转储,确认 UI 线程的堆栈。转动消息循环的线程通常是线程 0,但也存在拥有多条 UI 线程的应用,所以不要只凭编号判断,而要看卡死窗口所属的那条线程。
| 堆栈上看到的内容 | 接下来要调查的对象 |
|---|---|
停在 ReadFile 或网络 API 上等待 |
文件访问、同步 I/O、等待网络响应 |
停在 WaitFor… 系列上等待 |
锁或同步对象。在等什么完成,对方又在做什么 |
停在 SendMessage 内部等待 |
目标线程能否处理消息,双方是否在互相等待 |
UI 线程的堆栈上会呈现出它在等什么而停住。怀疑死锁时,不能只看一边,而要把两条线程的等待对象对照起来看。具体的读法在 WinDbg 入门文章中有讲解。
flowchart TB
accTitle: 无响应调查的基本步骤
accDescr: 在卡死的瞬间抓取转储,查看 UI 线程的堆栈,确定它是停在同步 I/O、等待锁还是跨线程 SendMessage 上,再连接到相应的设计修改
hang["卡死的瞬间"] --> dump["抓取转储(关掉之前)"]
dump --> stack["查看 UI 线程的堆栈"]
stack --> io["同步 I/O 与等待网络"]
stack --> lock["等待锁"]
stack --> sm["跨线程 SendMessage"]
io -.-> fix["把相应位置分离到工作线程"]
lock -.-> fix
sm -.-> fix
图4:从卡死瞬间的转储追查 UI 线程的等待对象。若是 I/O 就改为异步或分离,若是锁或 SendMessage 则要一直确认到等待汇合的结构。
7.3 区分使用实时查看与时序分析
除转储之外,还有当场查看运行中进程的方法,以及记录时间推移的方法。
| 想调查的内容 | 方法 |
|---|---|
| 保存卡死的瞬间,事后再调查 | 抓取转储并用 WinDbg 分析 |
| 当场查看存活进程的线程与堆栈 | 使用 Process Explorer |
| 按时序追踪长期存在的迟钝或 UI 线程的等待 | 用 WPR 采集跟踪,用 WPA 分析 |
时序数据的采集方法在 WPR/WPA 实战中有讨论。
7.4 由症状缩小候选,再用证据确认
复现条件也是调查的入口。不过不能只凭症状就断定原因,而要与转储或跟踪对照确认。
| 症状 | 首先怀疑的对象 | 确认的位置 |
|---|---|---|
| 做某个特定操作必定卡死 | 该处理程序内的同步 I/O 等 | 与该操作对应的处理,以及 UI 线程的堆栈 |
| 偶尔卡死,与操作没有相关性 | 锁的获取顺序、跨线程 SendMessage 的死锁 |
互相等待的两条线程的堆栈 |
| 只在特定环境下卡死 | 网络驱动器、代理、杀毒软件等造成的等待或超时 | 正在等待的 API,以及该环境下的响应时间 |
8. 总结
“无响应”是这样一套机制:Windows 判定窗口的消息处理已经停止,于是显示一个替代画面。判定的单位是窗口及其所属的 GUI 线程,而不是整个进程。5 秒这个内部超时是将来可能变动的值,要与令人感觉顺畅的 UI 响应时间区分开。12
要修的对象不是显示,而是长时间占用 UI 线程的计算或等待。把 CPU 计算和只有同步版本的 API 交给工作线程,把异步 I/O 交给异步 API,画面的更新则交回 UI 线程。进度、取消和重入防护也要配套设计。用 DoEvents 规避或禁用幽灵窗口,都替代不了这件事。
原因不明时,先从在结束进程之前抓取转储,查看卡死窗口所属线程在等什么开始。把“看起来像是坏了”换成“UI 线程回不到消息处理”,要查的位置和设计的修法就连起来了。
相关文章
- 虚假唤醒——条件变量为何会“未收到通知就醒来”,以及在 Windows 上正确的等待方法
- 多线程实务最佳实践 .NET 篇
- COM STA/MTA 的基础知识 - 线程模型与避免挂起的思路
- 用 WinDbg + SOS 解读崩溃转储——采集之后的实务分析入门
- Process Explorer / Handle / VMMap 实战——从此刻的状态追查挂起、泄漏与“文件正在使用中”
- 从应用看到的 Windows 关机——正确扛住退出通知、重启与断电
相关咨询领域
小村软件有限公司承接“偶尔卡死”“出现无响应”的业务应用的原因调查(转储解析与跟踪分析)、把满是同步处理的遗留 UI 代码改造为 async/await 并分离工作线程的改造,以及不会冻结的 UI 设计评审。即使还处在复现步骤不明的阶段,我们也可以从如何取证的设计开始提供帮助。
参考链接
-
Microsoft Learn, IsHungAppWindow function (winuser.h). 关于应用在“不在等待输入、不在启动处理中,且在内部超时的 5 秒内没有调用 PeekMessage”时被视为无响应的判定标准、这个 5 秒的基准可能发生变化,以及对幽灵窗口始终返回 TRUE。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, GetMessage function (winuser.h). 关于顶层窗口数秒不响应消息后,系统会将其视为无响应,并换成具有相同 Z 顺序、位置、大小和外观的幽灵窗口,用户只能进行移动、调整大小和关闭操作,以及附加了调试器时不会创建幽灵窗口。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). 关于 WinForms 的控件无法从创建它的线程以外安全操作、从其他线程更新时要使用 Invoke/BeginInvoke,以及使用 async/await 和 BackgroundWorker 的安全异步模式。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 关于对调用它的 GUI 进程,可以禁用让无响应窗口能够最小化、移动和关闭的幽灵窗口功能,以及该禁用在进程的整个生存期内持续。 ↩ ↩2
-
Microsoft Learn, About Messages and Message Queues. 关于 Windows 应用是事件驱动的、窗口过程处理消息的结构、经由队列的消息与直接发送的消息之间的区别、无响应窗口被换成幽灵窗口,以及线程之间互相发送消息导致死锁的那一节。 ↩ ↩2 ↩3
-
Microsoft Learn, Using Messages and Message Queues. 关于用 GetMessage、TranslateMessage、DispatchMessage 实现标准消息循环的示例,以及消息队列的调查方法。 ↩
-
Microsoft Learn, SendMessage function (winuser.h). 关于 SendMessage 会调用指定窗口的窗口过程并在处理完成前不返回、向另一条线程的窗口发送时会一直等到对方线程处理该消息,以及与不等待响应、直接放入队列的 PostMessage 之间的区别。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SendMessageTimeout function (winuser.h). 关于可以带超时发送消息,以及提供了对不响应的窗口(被判定为挂起的窗口)不等待即返回的标志(SMTO_ABORTIFHUNG)。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
DllMain 与加载器锁——“DLL 初始化里什么都别做”的真正原因
为什么不能在 DllMain 里调用 LoadLibrary 或与线程同步。本文依据一手资料,从串行化全部 DLL 通知的加载器锁机制,讲到死锁成立的典型场景、延迟初始化等正确设计,以及挂起的调查步骤。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
Windows 打印驱动程序停止提供 ── 业务应用的报表与标签打印如何应对
Microsoft 正在分阶段推进 v3/v4 打印驱动程序的停止提供,从 2026 年 7 月起会优先选择 IPP 类驱动程序。本文梳理 Windows protected print mode 下会消失什么,并用判断表整理业务应用程序的报表、标签打印中依赖点的盘点方法与...
Win32 线程池 API ── 用 CreateThreadpoolWork 实现「不自建线程」的并发
原生代码里是不是到处都在 CreateThread?本文依据一手资料讲解 Vista 全面重新设计的 Win32 线程池 API:work、timer、wait、io 四种对象,清理组,以及回调中禁止做的事。
睡眠恢复后就出故障的应用——电源事件机制与扛得住恢复的业务应用设计
打开笔记本电脑时业务应用的通信已经断开——原因是设计没有考虑睡眠。本文依据一手资料讲解 WM_POWERBROADCAST 的通知流程、Modern Standby 的行为、断开与重连的设计、睡眠抑制以及调查命令。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
UI 线程 & 计时器
整理 WPF / WinForms UI 线程、异步流程、Dispatcher 使用、计时器判断的主题页面。
常见问题
汇总了咨询这一主题时常见的问题。
- “无响应”是在什么条件下显示出来的?
- 当带窗口的应用“不在等待输入、不在启动处理中,且已 5 秒没有取出消息(PeekMessage)”时,操作系统就把该窗口判定为无响应。被判定的顶层窗口会被隐藏,并换成位置、大小和外观都相同的“幽灵窗口”。标题栏上的“(无响应)”以及发白模糊的外观都属于这个幽灵窗口,它只允许移动、最小化和关闭。也就是说,“无响应”不是应用自己显示的画面,而是操作系统代为显示的画面。
- 有没有让处理期间不显示“无响应”的设置?
- 调用 DisableProcessWindowsGhosting 可以对该进程禁用换成幽灵窗口的行为。但这只是“让用户不容易看出应用卡死了”,窗口不响应操作这个事实并没有改变,在用户看来反而成了既不能移动也不能关闭的彻底冻结。根本对策不是压制显示,而是把繁重处理移到工作线程,让 UI 线程别说 5 秒,就连 0.1 秒的量级也不要被堵住。另外还要注意,附加了调试器时操作系统不会创建幽灵窗口,因此看起来会像是“调试时不会出现无响应”。
- 可以用 DoEvents(手动转动消息泵)来规避无响应吗?
- 不推荐。在繁重处理的中途转动 DoEvents 或 PeekMessage 循环,确实能规避无响应判定,但按钮的二次点击、关闭窗口的操作、定时器等任意事件处理程序都会在处理中途重入。别的处理程序改写了正在处理的数据、触碰本应已关闭的窗体而抛出异常,这类重入缺陷依赖时机、难以复现,比无响应更棘手。正道是用 Task.Run 等把处理本身移到工作线程,让 UI 线程只负责显示进度和接受取消。
- 如何从工作线程更新界面(控件)?
- WinForms 的控件和 WPF 的元素只能从创建它们的线程(通常是 UI 线程)上操作。从工作线程直接操作会导致异常或未定义行为。C# 中最简单的做法是使用 async/await,await 的延续会回到调用方的 UI 线程,因此在 await 之后可以照常更新控件。需要显式切换时,WinForms 用 Control.Invoke/BeginInvoke,WPF 用 Dispatcher.InvokeAsync。在原生 Win32 中,惯用做法是工作线程用 PostMessage 向 UI 线程投递自定义的完成消息,由窗口过程一侧更新界面。
- 如何调查显示为“无响应”的应用的原因?
- 关键是在卡死的“那一刻”把状态取下来。先在任务管理器的详细信息选项卡用“创建转储文件”取得完整转储,再用 WinDbg 查看 UI 线程(转动消息循环的那条线程)的堆栈。究竟是停在同步 I/O、等待网络、等待锁,还是经由 SendMessage 等待其他线程,都会原样体现在堆栈上。要在存活状态下查看,可以用 Process Explorer 的线程列表和堆栈视图;要按时序追踪,用 WPR 采集跟踪很有效。也请参考本站的 WinDbg 入门、Process Explorer 实战和 WPR/WPA 实战各篇文章。