“无响应”的真相——Windows 如何判定应用卡死,以及不会卡死的设计

· 更新日期: · · Windows, Windows 开发, 故障排查, 多线程, WinForms, WPF, Win32 API, UI 设计

更新记录(仅首版,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

消息循环的基本结构操作系统把鼠标和键盘等输入放入线程的消息队列,UI 线程的循环用 GetMessage 取出并由 DispatchMessage 调用窗口过程,处理结束后回到循环开头操作系统(输入、重绘请求、定时器)线程的消息队列用 GetMessage 取出DispatchMessage由窗口过程处理

图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

跨线程 SendMessage 导致的死锁UI 线程因等待工作线程的结果而阻塞,此时工作线程向 UI 线程的窗口发送 SendMessage,两者互相等待对方完成,形成死锁工作线程UI 线程工作线程UI 线程处理不了消息(正在等待)无法从 SendMessage 返回互相等待,形成死锁等待工作线程完成(阻塞)SendMessage(处理完成前不返回)

图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

不会卡死的应用的职责划分UI 线程只负责接受输入、显示进度和接受取消,繁重处理由工作线程执行,完成后通过 PostMessage 或 await 的延续返回给 UI 线程交付工作PostMessage / await 的延续UI 线程:输入、进度、取消工作线程:繁重处理禁止在 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 入门文章中有讲解。

无响应调查的基本步骤在卡死的瞬间抓取转储,查看 UI 线程的堆栈,确定它是停在同步 I/O、等待锁还是跨线程 SendMessage 上,再连接到相应的设计修改卡死的瞬间抓取转储(关掉之前)查看 UI 线程的堆栈同步 I/O 与等待网络等待锁跨线程 SendMessage把相应位置分离到工作线程

图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 线程回不到消息处理”,要查的位置和设计的修法就连起来了。

相关文章

相关咨询领域

小村软件有限公司承接“偶尔卡死”“出现无响应”的业务应用的原因调查(转储解析与跟踪分析)、把满是同步处理的遗留 UI 代码改造为 async/await 并分离工作线程的改造,以及不会冻结的 UI 设计评审。即使还处在复现步骤不明的阶段,我们也可以从如何取证的设计开始提供帮助。

参考链接

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). 关于应用在“不在等待输入、不在启动处理中,且在内部超时的 5 秒内没有调用 PeekMessage”时被视为无响应的判定标准、这个 5 秒的基准可能发生变化,以及对幽灵窗口始终返回 TRUE。 ↩ ↩2 ↩3 ↩4

  2. Microsoft Learn, GetMessage function (winuser.h). 关于顶层窗口数秒不响应消息后,系统会将其视为无响应,并换成具有相同 Z 顺序、位置、大小和外观的幽灵窗口,用户只能进行移动、调整大小和关闭操作,以及附加了调试器时不会创建幽灵窗口。 ↩ ↩2 ↩3 ↩4 ↩5

  3. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). 关于 WinForms 的控件无法从创建它的线程以外安全操作、从其他线程更新时要使用 Invoke/BeginInvoke,以及使用 async/await 和 BackgroundWorker 的安全异步模式。 ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 关于对调用它的 GUI 进程,可以禁用让无响应窗口能够最小化、移动和关闭的幽灵窗口功能,以及该禁用在进程的整个生存期内持续。 ↩ ↩2

  5. Microsoft Learn, About Messages and Message Queues. 关于 Windows 应用是事件驱动的、窗口过程处理消息的结构、经由队列的消息与直接发送的消息之间的区别、无响应窗口被换成幽灵窗口,以及线程之间互相发送消息导致死锁的那一节。 ↩ ↩2 ↩3

  6. Microsoft Learn, Using Messages and Message Queues. 关于用 GetMessage、TranslateMessage、DispatchMessage 实现标准消息循环的示例,以及消息队列的调查方法。 ↩

  7. Microsoft Learn, SendMessage function (winuser.h). 关于 SendMessage 会调用指定窗口的窗口过程并在处理完成前不返回、向另一条线程的窗口发送时会一直等到对方线程处理该消息,以及与不等待响应、直接放入队列的 PostMessage 之间的区别。 ↩ ↩2 ↩3 ↩4

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). 关于可以带超时发送消息,以及提供了对不响应的窗口(被判定为挂起的窗口)不等待即返回的标志(SMTO_ABORTIFHUNG)。 ↩

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

“无响应”是在什么条件下显示出来的?
当带窗口的应用“不在等待输入、不在启动处理中,且已 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 实战各篇文章。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表