WPF 线程:使用调度程序构建反应速度更快的应用程序

简介: 原文:WPF 线程:使用调度程序构建反应速度更快的应用程序 作者:Shawn Wildermuth 原文:http://msdn.
原文: WPF 线程:使用调度程序构建反应速度更快的应用程序

作者:Shawn Wildermuth 
原文:http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=en-us

本文讨论:
  • 在 WPF 中执行线程
  • 使用调度程序
  • 非 UI 线程处理
  • 使用计时器
本文使用了以下技术:
.NET Framework 3.0, WIndows Presentation Foundation


目录


 

果您在创建一个直观、自然甚至精美的界面上花费了数月时间,但结果是用户不得不在他们的组合办公桌上敲打着手指等待程序响应,这会让人觉得丢脸。由于长时间运行的进程导致应用程序的屏幕停滞不动,看到这样的情况是一件痛苦的事情。然而,创建响应迅速的应用程序需要进行认真的规划,这通常需要使长时间运行的进程在其他线程中工作,以便释放出 UI 线程,使其随时跟上用户的进度。

 

我第一次真正体验响应速度可追溯到 Visual C++® 与 MFC 以及我曾经编写的第一个网格。当时,我正在帮助编写一个药学应用程序,该程序必须能够将每种药物显示在复杂的处方中。问题是有 30,000 种药物,因此我们决定先在 UI 线程中填充第一个满屏药物(时间大约为 50 毫秒),给人一种反应迅速的印象,然后使用后台线程完成填充不可见的药物(时间大约为 10 秒)。项目运行良好,而且我学到了非常宝贵的经验,那就是用户感知可以比现实更重要。

在创建具有吸引力的用户界面方面,Windows® Presentation Foundation (WPF) 是一项出色的技术,但这并不意味着您就不需要考虑应用程序的响应性。不管相关的长时间运行进程的类型为何(不管是从数据库获取大量结果,进行异步 Web 服务调用,还是任何数量的其他潜在密集型操作),简单的事实就是,响应更快的应用程序是让用户更满意的长期保证。但是,开始在 WPF 应用程序中使用异步编程模型之前,了解 WPF 线程模型非常重要。在本文中,我不但将会向您介绍此线程模型,还会向您展示基于调度程序的对象的工作原理,以及解释如何使用 BackgroundWorker 以便创建具有吸引力和响应性的用户界面。


线程模型

所有 WPF 应用程序启动时都会加载两个重要的线程:一个用于呈现用户界面,另一个用于管理用户界面。呈现线程是一个在后台运行的隐藏线程,因此您通常面对的唯一线程就是 UI 线程。WPF 要求将其大多数对象与 UI 线程进行关联。这称之为线程关联,意味着要使用一个 WPF 对象,只能在创建它的线程上使用。在其他线程上使用它会导致引发运行时异常。注意,WPF 线程模型可与基于 Win32® 的 API 进行顺畅的交互。这意味着 WPF 可以承载或承载于任何基于 HWND 的 API(Windows Forms、Visual Basic®、MFC,甚至是 Win32)。

线程关联由 Dispatcher 类处理,该类即是用于 WPF 应用程序的、按优先级排列的消息循环。通常,WPF 项目有单个 Dispatcher 对象(因此有单个 UI 线程),所有用户界面工作均以其为通道。

与典型的消息循环不同,发送到 WPF 的每个工作项目都以特定的优先级通过 Dispatcher 进行发送。这就能够按优先级对项目排序,并延迟某种类型的工作,直到系统有时间来处理它们。(例如,有些工作项目可被延迟到系统或应用程序处于空闲状态时。) 支持项目优先顺序使 WPF 能够让某种类型的工作拥有更多的权限,因此在线程上拥有比其他工作更多的时间。

在本文的后面,我将会阐明,呈现引擎在更新用户界面方面比输入系统具备更高的优先级。这意味着不管用户是否正在使用鼠标、键盘或墨水打印系统,动画都将会继续更新用户界面。这可以使用户界面看起来响应更快。例如,让我们假定您正在编写一个音乐播放应用程序(类似于 Windows Media® Player)。不管用户是否正在使用界面,您最有可能希望显示有关音乐播放的信息(包括进度条和其他信息)。对用户来说,这可以使界面看起来对他们最感兴趣的事情(在此例中为听音乐)响应更快。

除了使用 Dispatcher 的消息循环将工作项目引导至用户界面线程之外,每个 WPF 对象也可感知对其负责的 Dispatcher(以及它由此所依赖的 UI 线程)。这意味着任何从第二个线程更新 WPF 对象的尝试均会失败。这就是 DispatcherObject 类的职责。

Back to top

DispatcherObject

在 WPF 的类层次结构中,大部分都集中派生于 DispatcherObject 类(通过其他类)。如图 1 所示,您可以看到 DispatcherObject 虚拟类正好位于 Object 下方和大多数 WPF 类的层次结构之间。

图 1 Dispatcher­Object 派生
图 1  Dispatcher­Object 派生

DispatcherObject 类有两个主要职责:提供对对象所关联的当前 Dispatcher 的访问权限,以及提供方法以检查 (CheckAccess) 和验证 (VerifyAccess) 某个线程是否有权访问对象(派生于 DispatcherObject)。CheckAccess 与 VerifyAccess 的区别在于 CheckAccess 返回一个布尔值,表示当前线程是否可以使用对象,而 VerifyAccess 则在线程无权访问对象的情况下引发异常。通过提供这些基本的功能,所有 WPF 对象都支持对是否可在特定线程(特别是 UI 线程)上使用它们加以确定。如果您正在编写您自己的 WPF 对象(诸如控件),那么您使用的所有方法都应在执行任何工作之前调用 VerifyAccess。这可确保您的对象仅在 UI 线程上使用,如图 2 所示。

为此,在调用 Control、Window、Panel 之类的任何 DispatcherObject 派生对象时,应注意要处在 UI 线程上。如果您从非 UI 线程调用 DispatcherObject,就会引发异常。相反,如果您正在某个非 UI 线程上工作,就需要使用 Dispatcher 来更新 DispatcherObjects。

Back to top

使用调度程序

Dispatcher 类提供了到 WPF 中消息泵的通道,还提供了一种机制来路由供 UI 线程处理的工作。这对满足线程关联要求是必要的,但是对通过 Dispatcher 路由的每个工作来说,UI 线程都被阻止,因此使 Dispatcher 完成的工作小而快非常重要。最好将用户界面的大块工作拆分为较小的离散块,以便 Dispatcher 执行。任何不需要在 UI 线程上完成的工作应移到其他线程上,以便在后台进行处理。

通常,您将会使用 Dispatcher 类将工作项目发送到 UI 线程进行处理。例如,如果您想要使用 Thread 类在单独的线程上进行一些工作,那么可以创建一个 ThreadStart 委托,在新的线程上进行一些工作,如图 3 所示。

此代码执行失败,原因是当前没有在 UI 线程上调用对 statusText 控件(一种 TextBlock)的 Text 属性的设置。当该代码尝试设置 TextBlock 上的 Text 时,TextBlock 类会在内部调用其 VerifyAccess 方法以确保该调用来自 UI 线程。当它确定调用是来自不同的线程时,则会引发异常。那么您如何使用 Dispatcher 在 UI 线程上进行调用呢?

Dispatcher 类提供了在 UI 线程上直接调用代码的权限。图 4 展示了使用 Dispatcher 的 Invoke 方法来调用名叫 SetStatus 的方法,从而更改 TextBlock 的 Text 属性。

该 Invoke 调用包含三条信息:要执行的项目的优先级、说明要执行何种工作的委托,以及任何传递给第二个参数中所述委托的参数。通过调用 Invoke,它将要在 UI 线程上调用的委托排入队列。使用 Invoke 方法可确保在 UI 线程上执行工作之前保持阻止。

作为一种异步使用 Dispatcher 的替代方法,您可以使用 Dispatcher 的 BeginInvoke 方法为 UI 线程异步排队工作项目。调用 BeginInvoke 方法会返回一个 DispatcherOperation 类的实例,其中包含有关执行工作项目的信息,包括工作项目的当前状态和执行的结果(如果工作项目已完成)。BeginInvoke 方法和 DispatcherOperation 类的使用如图 5 所示。

与典型的消息泵实现不同,Dispatcher 是基于优先级的工作项目队列。这就能够实现更好的响应性,因为重要性更高的工作能够在重要性较低的工作之前执行。优先顺序的本质可通过 DispatchPriority 枚举中指定的优先级加以例证(如图 6 所示)。

一般来说,对于更新 UI 外观的工作项目(如我之前使用的示例),您应始终使用 DispatcherPriority.Normal 优先级。但也有时候应该使用不同的优先级。其中尤其令人感兴趣的是三个空闲优先级(ContextIdle、ApplicationIdle 和 SystemIdle)。通过这些优先级可以指定仅在工作负载很低的情况下执行的工作项目。

Back to top

BackgroundWorker

现在您对 Dispatcher 的工作原理已有所了解,那么如果得知在大多数情况下都不会使用它,您可能会感到惊讶。在 Windows Forms 2.0 中,Microsoft 引入了一个用于非 UI 线程处理的类来为用户界面开发人员简化开发模型。此类称为 BackgroundWorker。图 7 显示了 BackgroundWorker 类的典型用法。

BackgroundWorker 组件与 WPF 的配合非常好,因为在后台它使用了 AsyncOperationManager 类,该类随之又使用 SynchronizationContext 类来处理同步。在 Windows Forms 中,AsyncOperationManager 递交从 SynchronizationContext 类派生的 WindowsFormsSynchronizationContext 类。同样,在 ASP.NET 中,它与 SynchronizationContext 的不同派生(称为 AspNetSynchronizationContext)配合使用。这些 SynchronizationContext 派生的类知道如何处理方法调用的跨线程同步。

在 WPF 中,可用 DispatcherSynchronizationContext 类来扩展此模型。通过使用 BackgroundWorker,可自动应用 Dispatcher 来调用跨线程方法调用。好消息是,由于您可能已经熟悉了这个常见的模式,因此可以继续在新的 WPF 项目中使用 BackgroundWorker。

Back to top

DispatcherTimer

在 Microsoft® .NET Framework 中定期执行代码是开发中的一项常见任务,但是在 .NET 中使用计时器仍令人困惑。如果您在 .NET Framework 基类库 (BCL) 中查找 Timer 类,那么至少会找到 3 种 Timer 类:System.Threading.Timer、System.Timers.Timer 和 System.Windows.Forms.Timer。每种计时器均有所不同。Alex Calvo 在《MSDN 杂志》中的文章解释了何时使用这些 Timer 类中的每个类(请参见 msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET)。

对于 WPF 应用程序来说,有一种使用 Dispatcher(即 DispatcherTimer 类)的新型计时器。与其他计时器类似,DispatcherTimer 类支持指定滴答之间的间隔,以及在计时器事件触发时要运行的代码。在图 8 中可以看到一种相当常见的 DispatcherTimer 使用方法。

因为 DispatcherTimer 类与 Dispatcher 相关联,因此还可以指定 DispatcherPriority 以及要使用的 Dispatcher。DispatcherTimer 类使用“正常”优先级作为当前 Dispatcher 的默认优先级,但是您可以覆盖这些值:

 

_timer = new DispatcherTimer(
    DispatcherPriority.SystemIdle, form1.Dispatcher);

 

规划工作进程以获得响应更快的应用程序,其中的一切努力都是非常值得的。开展一些初期研究工作可以使规划更成功。我建议您在开始之前浏览一下“WPF 线程参考”侧栏中提到的一些网站以及本文章,它们会为您开发响应更快的应用程序打下良好的基础。 

目录
相关文章
|
2月前
|
Java 开发者
如何通过易语言多线程提升程序响应速度
如何通过易语言多线程提升程序响应速度
152 62
|
3月前
|
C# 开发者 Windows
WPF 应用程序开发:一分钟入门
本文介绍 Windows Presentation Foundation (WPF),这是一种用于构建高质量、可缩放的 Windows 桌面应用程序的框架,支持 XAML 语言,方便 UI 设计与逻辑分离。文章涵盖 WPF 基础概念、代码示例,并深入探讨常见问题及解决方案,包括数据绑定、控件样式与模板、布局管理等方面,帮助开发者高效掌握 WPF 开发技巧。
166 65
|
2月前
|
Java 开发者
如何通过易语言多线程提升程序响应速度?
如何通过易语言多线程提升程序响应速度?
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
51 4
|
2月前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,它可能导致应用程序性能下降,甚至使整个系统变得不稳定。
线程死循环是多线程编程中常见的问题,可能导致性能下降或系统不稳定。通过代码审查、静态分析、日志监控、设置超时、使用锁机制、测试、选择线程安全的数据结构、限制线程数、使用现代并发库及培训,可有效预防和解决死循环问题。
62 1
|
2月前
|
监控 Java API
|
4月前
|
C# 微服务 Windows
模块化革命:揭秘WPF与微服务架构的完美融合——从单一职责原则到事件聚合器模式,构建高度解耦与可扩展的应用程序
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中借鉴微服务架构思想,实现模块化设计。通过将WPF应用分解为独立的功能模块,并利用事件聚合器实现模块间解耦通信,可以有效提升开发效率和系统可维护性。文中还提供了具体示例代码,展示了如何使用事件聚合器进行模块间通信,以及如何利用依赖注入进一步提高模块解耦程度。此方法不仅有助于简化复杂度,还能使应用更加灵活易扩展。
105 0
|
4月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
94 0
|
4月前
|
C# 开发者 Windows
震撼发布:全面解析WPF中的打印功能——从基础设置到高级定制,带你一步步实现直接打印文档的完整流程,让你的WPF应用程序瞬间升级,掌握这一技能,轻松应对各种打印需求,彻底告别打印难题!
【8月更文挑战第31天】打印功能在许多WPF应用中不可或缺,尤其在需要生成纸质文档时。WPF提供了强大的打印支持,通过`PrintDialog`等类简化了打印集成。本文将详细介绍如何在WPF应用中实现直接打印文档的功能,并通过具体示例代码展示其实现过程。
333 0