对 WPF 渲染线程失败进行故障排除

本文讨论 Windows Presentation Foundation (WPF) 呈现线程中的失败。 本文重点介绍发生在SyncFlushNotifyPartitionIsZombie中的异常,和发生在WaitForNextMessageSynchronizeChannel中的挂起情况。

WPF 应用程序可能有一个或多个运行自己的消息泵的 UI 线程(Dispatcher.Run)。 每个 UI 线程负责处理线程的消息队列中的窗口消息,并将其调度到线程拥有的窗口。 每个 WPF 应用程序只有一个呈现线程。 如果使用软件呈现管道,该单独的线程与 Microsoft DirectX D3D(或 GDI)通信。 对于 WPF 内容,每个 UI 线程都会向要绘制的内容的呈现线程发送详细说明。 然后,呈现线程按照这些说明呈现内容。

适用于: .NET Framework 4.8

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 中的失败

开发人员经常遇到与 WPF 应用程序中发生的线程故障相关的问题。 用户可能会报告其应用程序引发异常,例如:

  • System.Runtime.InteropServices.COMException:UCEERR_RENDERTHREADFAILURE(HRESULT 异常:0x88980406)
  • System.InvalidOperationException:呈现线程上出现未指定的错误。
  • System.OutOfMemoryException:内存不足,无法继续执行程序。

相关调用堆栈从 SyncFlushNotifyPartitionIsZombie 开始。 例如:

   at System.Windows.Media.Composition.DUCE.Channel.SyncFlush()  
   at System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget, Nullable\`1 channelSet)  
   at System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean enableRenderTarget)  
   at System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr lParam)  
   at System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)  
   at System.Windows.Media.MediaContext.NotifyPartitionIsZombie(Int32 failureCode)  
   at System.Windows.Media.MediaContext.NotifyChannelMessage()  
   at System.Windows.Interop.HwndTarget.HandleMessage(Int32 msg, IntPtr wparam, IntPtr lparam)  

应用程序可能会在WaitForNextMessageSynchronizeChannel 停止响应,并生成调用堆栈,例如:

   ntdll.dll!NtWaitForMultipleObjects
   kernelbase.dll!WaitForMultipleObjectsEx
   kernelbase.dll!WaitForMultipleObjects
   wpfgfx_v0400.dll!CMilChannel::WaitForNextMessage
   wpfgfx_v0400.dll!MilComposition_WaitForNextMessage
   presentationcore.dll!System.Windows.Media.MediaContext.CompleteRender
   kernelbase.dll!WaitForSingleObject
   wpfgfx_v0400.dll!CMilConnection::SynchronizeChannel
   wpfgfx_v0400.dll!CMilChannel::SyncFlush
   presentationcore.dll!System.Windows.Media.Composition.DUCE+Channel.SyncFlush
   presentationcore.dll!System.Windows.Media.MediaContext.CompleteRender
   presentationcore.dll!System.Windows.Interop.HwndTarget.OnResize
   presentationcore.dll!System.Windows.Interop.HwndTarget.HandleMessage

这些调用堆栈是渲染线程失败的症状。 这是诊断的一个具有挑战性的问题,因为异常和调用堆栈是泛型的。 无论根本原因如何,呈现线程失败都会生成此处列出的调用堆栈之一(或调用堆栈的细微变化)。 因此,很难诊断由不响应事件引发的问题,或识别具有相同根本原因的不同非响应事件。

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 中失败的原因

在 WPF 呈现线程发生致命错误的情况下,异常和软件在 UI 线程中停止响应的情况会出现。 这些错误有多种可能的原因,但呈现线程不会与 UI 线程共享该信息。 由于这些错误与单个根 bug 或问题无关,因此它们没有特定的解决方案。

WPF 的呈现线程在调用其他组件(例如 DirectX D3D、User32 或 GDI32)时检查返回值是否成功或失败。 检测到故障时,WPF 会将渲染分区置于“僵尸”状态,并在两个线程同步时通知 UI 线程发生故障。 呈现线程尝试将它收到的失败映射到适当的托管异常。 例如,如果 WPF 呈现线程因内存不足而失败,则会将失败映射到 a System.OutOfMemoryException。 该异常显示在 UI 线程上。 呈现线程仅在几个位置与 UI 线程同步。 因此,上一节中提到的调用堆栈通常会显示在你注意到问题症状的位置,而不是问题实际发生的地方。 同步操作最常发生在窗口设置被更新(如大小、位置等)时,或者UI线程处理从呈现线程收到的“通道”消息时。

根据设计,UI 线程上的异常和调用堆栈不是有用的资源,有助于诊断问题。 在引发异常时,渲染线程已经过了故障点。 呈现线程的关键状态将帮助你了解失败发生的位置和原因,但它已经丢失。 由于这种情况,WPF 应用程序的作者不知道发生失败的原因或如何避免它。 我们调试事后用户转储文件中的问题,而不是分析异常和调用堆栈。 尽管这种方法只是稍微有用,但渲染线程会保留失败调用堆栈的一个循环缓冲区。 我们可以使用专有调试器扩展和专用调试符号在内部重新构造缓冲区,以显示大致的初始故障点。 但是,在发生故障时,我们无权访问关键状态,例如局部变量、堆栈变量和堆对象。 我们通常再次运行应用程序,以查找我们怀疑涉及的调用失败。

视频硬件或驱动程序故障

WPF 呈现线程故障的最常见存储桶与视频硬件或驱动程序问题相关联。 当 WPF 通过 DirectX 查询视频驱动程序以获取功能时,驱动程序可能会错误地报告其功能。 此操作会导致 WPF 执行导致某些 DirectX D3D 失败的代码路径。 驱动程序也可能未正确实现。 大多数呈现线程失败都是因为 WPF 尝试以暴露驱动程序中某些缺陷的方式使用硬件呈现管道。 这种情况可能发生在使用现代图形设备和驱动程序的现代版本的 Windows 上,不过并不像 WPF 早期那么常见。 因此,在开始测试或解决呈现线程故障时,建议先在 WPF 中禁用硬件加速。

如果应用请求场景过于复杂,驱动程序(或 DirectX)无法呈现,也可能发生故障。 对于新式驱动程序来说,这种情况并不常见。 但是,每个设备都有可以超过的限制。

呈现线程失败的另一个历史源是 WPF 中的 Window.AllowsTransparencyPopup.AllowsTransparency 属性。 这些属性会导致使用 分层窗口 。 分层窗口在较旧版本的 Windows 中造成了问题。 但是,在 Windows Vista 中引入桌面窗口管理器(DWM)解决了其中大多数问题。

如果呈现线程故障显示为一个 System.OutOfMemoryException,该错误通常表示进程耗尽了一些资源。 在这种情况下,呈现线程调用了Win32/DX API,该 API 尝试分配资源但失败了。 WPF 将返回值,例如 E_OUTOFMEMORYERROR_NOT_ENOUGH_MEMORY 映射到 System.OutOfMemoryException。 尽管异常项是指“内存”,但此提及可以引用任何类型的资源,例如 GDI 对象句柄、其他系统句柄、GPU 内存、标准 RAM 内存等。

有关资源分配失败的备注

以下注释适用于 System.OutOfMemoryException 失败和任何资源分配失败:

  • 根本原因可能与遇到故障的代码无关。 进程中的其他代码可能会过度使用资源,导致没有剩余资源可供本应成功运行的代码使用。

  • 如果请求异常大,则尽管资源看似丰富,但可能会发生失败。 即使系统有大量内存,对大量(连续)内存的请求仍可能导致 System.OutOfMemoryException。 下面是一个实际示例:Visual Studio 加载项准备从上一会话中保存的状态还原其窗口。 加载项无法正确调整以前的监视器和当前监视器之间的 DPI 差异。 由于此错误因 WPF、WindowsForms 和 VS 窗口托管组件的多个层的调整而复合,因此外接程序将其窗口大小设置为大于正确大小的 16 倍。 然后,呈现线程尝试分配比必要大 256 倍的后缓冲区。 因此,即使有足够的可用内存用于预期的分配,进程也会失败。

一般建议

  1. 停用硬件渲染。 使用禁用硬件加速选项中讨论的 DisableHWAcceleration 注册表值。 此操作会影响您计算机上的所有 WPF 应用程序。 仅执行此步骤以测试问题是否与图形硬件或驱动程序相关。 如果是,可以通过以编程方式在更精细的级别上禁用硬件加速来解决此问题。 可以通过使用 HwndTarget.RenderMode 属性在每个窗口的基础上执行此步骤,或者通过使用 RenderOptions.ProcessRenderMode 属性在每个进程的基础上执行。

  2. 更新视频驱动程序,或在问题计算机上尝试不同的视频硬件。

  3. 升级到适用于目标平台的 Microsoft .NET Framework 的最新版本和 Service Pack 级别。

  4. 升级到最新的操作系统。

  5. 禁用在应用程序中使用 Windows.AllowsTransparencyPopup.AllowsTransparency 的功能。

  6. 如果报告 System.OutOfMemoryExceptions,请监视进程在性能监视器中的内存使用情况。 具体而言,监控这些计数器:Process\Virtual Bytes、Process\Private Bytes,以及.NET CLR Memory\# Bytes in All Heaps。 此外,监视 Windows 任务管理器中进程的用户对象和 GDI 对象。 如果确定某个特定资源正在耗尽,请对应用程序进行故障排除,以解决过度的资源消耗问题。 作为指导原则,请参考上一部分中关于资源分配问题的两条言论。

  7. 如果在跨平台或不同视频硬件或驱动程序组合中出现可重现的情景,可能是 WPF 的一个 bug。 确保在向微软提交问题报告之前,收集足够的信息以便开展调查。 调用堆栈本身是不够的。 我们需要更详细的信息,例如:

    • 完整的 VS 解决方案,其中包括重现问题的步骤,包括环境描述(OS、.NET 和图形)。
    • 问题的时间旅行调试跟踪
    • 完整故障转储文件。