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.AllowsTransparency 或 Popup.AllowsTransparency 属性,这将导致使用 分层窗口 。 较旧版本的 Windows 在分层窗口方面存在问题,但大多数问题已通过在 Windows Vista 中引入桌面窗口管理器 (DWM) 得到解决。
如果呈现线程故障显示为 System.OutOfMemoryException
,则呈现线程可能是进程耗尽某些资源的受害者。 呈现线程调用到 Win32/DX
尝试分配某些资源的 API 中,但失败。 WPF 将返回值(如 E_OUTOFMEMORY
或 ERROR_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.AllowsTransparency
和 Popup.AllowsTransparency
。
如果System.OutOfMemoryExceptions
报告了,请在 性能监视器 中监视进程的内存使用情况;尤其是所有堆计数器中的 Process\Virtual Bytes、Process\Private Bytes 和 .NET CLR Memory\# Bytes。 在 Windows 任务管理器中监视进程的用户对象和 GDI 对象。 如果确定特定资源正在耗尽,请对应用程序进行故障排除,以修复过度的资源消耗。 请记住上面关于资源分配问题的两句话。
如果具有跨平台或不同视频硬件/驱动程序组合的可重现方案,则可能存在 WPF bug。 在向 Microsoft 报告问题之前,请务必收集足够的信息,以便进行调查。 调用堆栈不够。 Microsoft 需要更详细的信息,例如:
完整的 VS 解决方案,其中包含重现问题的步骤,包括环境描述 - OS、.NET 和图形。
问题的 “时间行程调试”跟踪 。
完全故障转储。