我第一次真正体验响应速度可追溯到 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 类的职责。
DispatcherObject
在 WPF 的类层次结构中,大部分都集中派生于 DispatcherObject 类(通过其他类)。如图 1 所示,您可以看到 DispatcherObject 虚拟类正好位于 Object 下方和大多数 WPF 类的层次结构之间。
派生
DispatcherObject 类有两个主要职责:提供对对象所关联的当前 Dispatcher 的访问权限,以及提供方法以检查 (CheckAccess) 和验证 (VerifyAccess) 某个线程是否有权访问对象(派生于 DispatcherObject)。CheckAccess 与 VerifyAccess 的区别在于 CheckAccess 返回一个布尔值,表示当前线程是否可以使用对象,而 VerifyAccess 则在线程无权访问对象的情况下引发异常。通过提供这些基本的功能,所有 WPF 对象都支持对是否可在特定线程(特别是 UI 线程)上使用它们加以确定。如果您正在编写您自己的 WPF 对象(诸如控件),那么您使用的所有方法都应在执行任何工作之前调用 VerifyAccess。这可确保您的对象仅在 UI 线程上使用
public class MyWpfObject : DispatcherObject
{
public void DoSomething()
{
VerifyAccess();
// Do some work
}
public void DoSomethingElse()
{
if (CheckAccess())
{
// Something, only if called
// on the right thread
}
}
}
为此,在调用 Control、Window、Panel 之类的任何 DispatcherObject 派生对象时,应注意要处在 UI 线程上。
如果您从非 UI 线程调用 DispatcherObject,就会引发异常。
相反,如果您正在某个非 UI 线程上工作,就需要使用 Dispatcher 来更新 DispatcherObjects。
续下 .....