WPF 呈现线程失败

本文档讨论 WPF 的呈现线程中的失败,特别要注意那些导致应用程序挂起或导致应用程序处于或NotifyPartitionIsZombie挂起WaitForNextMessageSynchronizeChannel状态的故障SyncFlush

原始产品版本: .NET Framework 4.8

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 中的失败

开发人员经常遇到与 Windows Presentation Foundation (WPF) 应用程序呈现线程失败相关的问题。 用户可以报告其应用程序引发异常,例如:

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

异常的调用堆栈从 SyncFlush 或开始 NotifyPartitionIsZombie。 例如:

   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

这些是呈现线程中失败的症状。 这是诊断的一个具有挑战性的问题,因为收到的异常和调用堆栈是泛型的。 无论根本原因如何,呈现线程失败都会生成上面所示的调用堆栈之一(或其次要变体)。 这使得诊断问题,甚至识别两个崩溃或挂起源于同一根本原因,尤其困难。

WPF 呈现线程及其与 UI 线程有何不同的说明

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

上述失败的原因

上述异常和挂起发生在 UI 线程中,因为 WPF 呈现线程遇到致命错误。 这些错误有多种可能的原因,但呈现线程不会与 UI 线程共享该信息。 由于这些异常和挂起不源于单个根 bug 或问题,因此没有具体方法可以修复它们。

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

根据设计,UI 线程上的异常和调用堆栈对诊断问题没有帮助。 这是因为在引发异常时,呈现线程已传递故障点。 呈现线程的关键状态将帮助我们了解失败发生的位置和原因,但已丢失。 这使得编写 WPF 应用程序的人几乎不可能知道失败的原因或如何避免它。 对于Microsoft,在验尸后用户转储文件中调试此功能只会稍好一些。 呈现线程保留故障调用堆栈的循环缓冲区,我们可以通过专有调试器扩展和专用调试符号在内部重新构造,以显示大致的初始故障点。 但是,在发生故障时,我们无权访问关键状态,例如局部变量、堆栈变量和堆对象。 我们通常再次运行应用程序,以查找我们怀疑涉及的调用失败。

失败的常见原因

WPF 呈现线程故障的最常见存储桶与视频硬件或驱动程序问题相关联。 当 WPF 通过 DirectX 查询视频驱动程序以获取功能时,驱动程序可能会错误地报告其功能,导致 WPF 采用最终导致某些 DirectX/D3D 故障的代码路径。 有时,驱动程序不会错误地报告其功能,但未正确实现。 大多数呈现线程故障是由 WPF 尝试以在驱动程序中暴露一些缺陷的方式利用硬件呈现管道引起的。 这可能发生在具有新式图形设备和驱动程序的新式 Windows 版本上,尽管它不像 WPF 早期那样常见。 这就是为什么我们在 WPF 中禁用硬件加速的第一个测试和/或解决呈现线程故障的建议之一。

也可能是应用要求呈现过于复杂的场景(或 DirectX)无法处理的场景导致的故障。 这与新式驱动程序并不常见,但每个设备都有限制,并且无法超过它们。

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

如果呈现线程故障显示为呈现 System.OutOfMemoryException线程,则呈现线程可能是进程耗尽某些资源的受害者。 在尝试分配某些资源的 API 中 Win32/DX 调用呈现线程,但失败。 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. 升级到适用于目标平台的 .NET 的最新版本和 Service Pack 级别。

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

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

  6. 如果System.OutOfMemoryExceptions报告,请监视进程性能监视器中的内存使用情况;尤其是进程\虚拟字节、进程\专用字节和所有堆计数器中的 .NET CLR 内存\# 字节数。 监视 Windows 任务管理器中进程的用户对象和 GDI 对象。 如果确定特定资源已用尽,请对应用程序进行故障排除,以解决过度的资源消耗问题。 请记住上述两个关于资源分配问题的言论。

  7. 如果具有跨平台或不同视频硬件/驱动程序组合发生的可重现方案,则可能会出现 WPF bug。 请务必收集足够的信息,以便在向Microsoft报告问题之前允许调查。 调用堆栈不够。 Microsoft需要更详细的信息,例如:

    • 完整的 VS 解决方案,其中包含重现问题的步骤,包括环境说明 -OS、.NET 和图形。
    • 问题的时间旅行调试跟踪
    • 完全故障转储。