本文档讨论 WPF 呈现线程中的故障,并特别注意那些导致 或 SyncFlush NotifyPartitionIsZombie 中的异常或导致应用程序挂起 WaitForNextMessage SynchronizeChannel 的故障。

原始产品版本: .NET Framework 4.8

SyncFlush、WaitForNextMessage、SynchronizeChannel 和 NotifyPartitionIsZombie 中的故障

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

  • System.Runtime.InteropServices.COMException:HRESULT 中的UCEERR_RENDERTHREADFAILURE (异常: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)  
    

    应用程序还可以在 或 SynchronizeChannelWaitForNextMessage挂起,其调用堆栈如下:

       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 线程都会向呈现线程发送有关绘制内容的详细说明。 然后,呈现线程接受这些指令并呈现内容。

    上述失败的原因

    由于 WPF 呈现线程遇到严重错误,上述异常和挂起发生在 UI 线程中。 这些错误的可能原因有多种,但呈现线程不会与 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,则呈现线程可能是进程耗尽某些资源的受害者。 呈现线程调用到 Win32/DX 尝试分配某些资源的 API 中,但失败。 WPF 将返回值(如 E_OUTOFMEMORYERROR_NOT_ENOUGH_MEMORY )映射到 System.OutOfMemoryException。 尽管异常是指“内存”,但失败可能指任何类型的资源,例如 GDI 对象句柄、其他系统句柄、GPU 内存、普通 RAM 内存等。

    有关资源分配失败的备注

    两个注释适用于 System.OutOfMemoryException 失败和任何资源分配失败。

  • 根本原因可能不在于遇到失败的代码。 相反,进程中可能还有其他代码会过度使用资源,使正常成功的请求不会留下任何代码。

  • 如果请求非常大,则尽管资源看起来很充足,但可能会失败。 System.OutOfMemoryException即使系统具有足够的内存,如果请求大量 (连续) 内存,也可能发生 。 下面是一个实际示例:Visual Studio 插件正准备从上一个会话中保存的状态还原其窗口。 它针对以前监视器和当前监视器之间的 DPI 差异进行了错误调整,这与 WPF、WindowsForms 和 VS 窗口托管组件的多个层进行了调整,以将其窗口的大小设置为原应大 16 倍。 呈现线程尝试分配一个比所需大 256 倍的后缓冲区,即使有足够内存可用于预期分配,也会失败。

  • 使用禁用硬件加速选项中讨论的 DisableHWAcceleration 注册表值禁用硬件呈现。 这将影响计算机上的所有 WPF 应用程序;这样做只是为了测试问题是否与图形硬件或驱动程序相关。 如果是这种情况,可以通过以编程方式在更精细的级别禁用硬件加速来解决此问题。 这可以通过使用 HwndTarget.RenderMode 属性在每窗口的基础上完成,也可以使用 RenderOptions.ProcessRenderMode 属性在每个进程的基础上完成。

  • 更新视频驱动程序,和/或尝试问题计算机中的不同视频硬件 () 。

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

  • 升级到最新的操作系统。

  • 禁止在应用程序中使用 Windows.AllowsTransparencyPopup.AllowsTransparency

  • 如果System.OutOfMemoryExceptions报告了,请在 性能监视器 中监视进程的内存使用情况;尤其是所有堆计数器中的 Process\Virtual Bytes、Process\Private Bytes 和 .NET CLR Memory\# Bytes。 在 Windows 任务管理器中监视进程的用户对象和 GDI 对象。 如果确定特定资源正在耗尽,请对应用程序进行故障排除,以修复过度的资源消耗。 请记住上面关于资源分配问题的两句话。

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

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