WPF线程,使用调度程序构建反应速度更快的应用程序
by:Shawn Wildermuth
From:http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#
如果您在创建一个直观、自然甚至精美的界面上花费了数月时间,但结果是用户不得不在他们的组合办公桌上敲打着手指等待程序响应,这会让人觉得丢脸。由于长时间运行的进程导致应用程序的屏幕停滞不动,看到这样的情况是一件痛苦的事情。然而,创建响应迅速的应用程序需要进行认真的规划,这通常需要使长时间运行的进程在其他线程中工作,以便释放出 UI 线程,使其随时跟上用户的进度。
® 与 MFC 以及我曾经编写的第一个网格。当时,我正在帮助编写一个药学应用程序,该程序必须能够将每种药物显示在复杂的处方中。问题是有 30,000 种药物,因此我们决定先在 UI 线程中填充第一个满屏药物(时间大约为 50 毫秒),给人一种反应迅速的印象,然后使用后台线程完成填充不可见的药物(时间大约为 10 秒)。项目运行良好,而且我学到了非常宝贵的经验,那就是用户感知可以比现实更重要。
® Presentation Foundation (WPF) 是一项出色的技术,但这并不意味着您就不需要考虑应用程序的响应性。不管相关的长时间运行进程的类型为何(不管是从数据库获取大量结果,进行异步 Web 服务调用,还是任何数量的其他潜在密集型操作),简单的事实就是,响应更快的应用程序是让用户更满意的长期保证。但是,开始在 WPF 应用程序中使用异步编程模型之前,了解 WPF 线程模型非常重要。在本文中,我不但将会向您介绍此线程模型,还会向您展示基于调度程序的对象的工作原理,以及解释如何使用 BackgroundWorker 以便创建具有吸引力和响应性的用户界面。
®、MFC,甚至是 Win32)。
线程关联由 Dispatcher 类处理,该类即是用于 WPF 应用程序的、按优先级排列的消息循环。通常,WPF 项目有单个 Dispatcher 对象(因此有单个 UI 线程),所有用户界面工作均以其为通道。
与典型的消息循环不同,发送到 WPF 的每个工作项目都以特定的优先级通过 Dispatcher 进行发送。这就能够按优先级对项目排序,并延迟某种类型的工作,直到系统有时间来处理它们。(例如,有些工作项目可被延迟到系统或应用程序处于空闲状态时。) 支持项目优先顺序使 WPF 能够让某种类型的工作拥有更多的权限,因此在线程上拥有比其他工作更多的时间。
® Player)。不管用户是否正在使用界面,您最有可能希望显示有关音乐播放的信息(包括进度条和其他信息)。对用户来说,这可以使界面看起来对他们最感兴趣的事情(在此例中为听音乐)响应更快。
除了使用 Dispatcher 的消息循环将工作项目引导至用户界面线程之外,每个 WPF 对象也可感知对其负责的 Dispatcher(以及它由此所依赖的 UI 线程)。这意味着任何从第二个线程更新 WPF 对象的尝试均会失败。这就是 DispatcherObject 类的职责。
图 1 所示,您可以看到 DispatcherObject 虚拟类正好位于 Object 下方和大多数 WPF 类的层次结构之间。
图 1 DispatcherObject 派生
图 2 所示。
为此,在调用 Control、Window、Panel 之类的任何 DispatcherObject 派生对象时,应注意要处在 UI 线程上。如果您从非 UI 线程调用 DispatcherObject,就会引发异常。相反,如果您正在某个非 UI 线程上工作,就需要使用 Dispatcher 来更新 DispatcherObjects。
Dispatcher 类提供了到 WPF 中消息泵的通道,还提供了一种机制来路由供 UI 线程处理的工作。这对满足线程关联要求是必要的,但是对通过 Dispatcher 路由的每个工作来说,UI 线程都被阻止,因此使 Dispatcher 完成的工作小而快非常重要。最好将用户界面的大块工作拆分为较小的离散块,以便 Dispatcher 执行。任何不需要在 UI 线程上完成的工作应移到其他线程上,以便在后台进行处理。
图 3 所示。
此代码执行失败,原因是当前没有在 UI 线程上调用对 statusText 控件(一种 TextBlock)的 Text 属性的设置。当该代码尝试设置 TextBlock 上的 Text 时,TextBlock 类会在内部调用其 VerifyAccess 方法以确保该调用来自 UI 线程。当它确定调用是来自不同的线程时,则会引发异常。那么您如何使用 Dispatcher 在 UI 线程上进行调用呢?
图 4 展示了使用 Dispatcher 的 Invoke 方法来调用名叫 SetStatus 的方法,从而更改 TextBlock 的 Text 属性。
该 Invoke 调用包含三条信息:要执行的项目的优先级、说明要执行何种工作的委托,以及任何传递给第二个参数中所述委托的参数。通过调用 Invoke,它将要在 UI 线程上调用的委托排入队列。使用 Invoke 方法可确保在 UI 线程上执行工作之前保持阻止。
图 5 所示。
图 6 所示)。
一般来说,对于更新 UI 外观的工作项目(如我之前使用的示例),您应始终使用 DispatcherPriority.Normal 优先级。但也有时候应该使用不同的优先级。其中尤其令人感兴趣的是三个空闲优先级(ContextIdle、ApplicationIdle 和 SystemIdle)。通过这些优先级可以指定仅在工作负载很低的情况下执行的工作项目。
图 7 显示了 BackgroundWorker 类的典型用法。
BackgroundWorker 组件与 WPF 的配合非常好,因为在后台它使用了 AsyncOperationManager 类,该类随之又使用 SynchronizationContext 类来处理同步。在 Windows Forms 中,AsyncOperationManager 递交从 SynchronizationContext 类派生的 WindowsFormsSynchronizationContext 类。同样,在 ASP.NET 中,它与 SynchronizationContext 的不同派生(称为 AspNetSynchronizationContext)配合使用。这些 SynchronizationContext 派生的类知道如何处理方法调用的跨线程同步。
在 WPF 中,可用 DispatcherSynchronizationContext 类来扩展此模型。通过使用 BackgroundWorker,可自动应用 Dispatcher 来调用跨线程方法调用。好消息是,由于您可能已经熟悉了这个常见的模式,因此可以继续在新的 WPF 项目中使用 BackgroundWorker。
msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET)。
图 8 中可以看到一种相当常见的 DispatcherTimer 使用方法。
因为 DispatcherTimer 类与 Dispatcher 相关联,因此还可以指定 DispatcherPriority 以及要使用的 Dispatcher。DispatcherTimer 类使用“正常”优先级作为当前 Dispatcher 的默认优先级,但是您可以覆盖这些值:
_timer = new DispatcherTimer(
DispatcherPriority.SystemIdle, form1.Dispatcher);
规划工作进程以获得响应更快的应用程序,其中的一切努力都是非常值得的。开展一些初期研究工作可以使规划更成功。我建议您在开始之前浏览一下“WPF 线程参考”侧栏中提到的一些网站以及本文章,它们会为您开发响应更快的应用程序打下良好的基础。
--------------------------------------------------
图2
public class MyWpfObject : DispatcherObject
{
public void DoSomething()
{
VerifyAccess();
// Do some work
}
public void DoSomethingElse()
{
if (CheckAccess())
{
// Something, only if called
// on the right thread
}
}
}
Figure 3 用非 UI 线程更新 UI——错误的方法
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// This will throw an exception
// (it's on the wrong thread)
statusText.Text = "From Other Thread";
};
// Create the thread and kick it started!
new Thread(start).Start();
更新 UI
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// Sets the Text on a TextBlock Control.
// This will work as its using the dispatcher
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action<string>(SetStatus),
"From Other Thread");
};
// Create the thread and kick it started!
new Thread(start).Start();
异步更新 UI
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// This will work as its using the dispatcher
DispatcherOperation op = Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new Action<string>(SetStatus),
"From Other Thread (Async)");
DispatcherOperationStatus status = op.Status;
while (status != DispatcherOperationStatus.Completed)
{
status = op.Wait(TimeSpan.FromMilliseconds(1000));
if (status == DispatcherOperationStatus.Aborted)
{
// Alert Someone
}
}
};
// Create the thread and kick it started!
new Thread(start).Start();
DispatchPriority 优先级别(按优先级次序)
| 优先级 | 说明 |
|---|---|
| 非活动 | 工作项目已排队但未处理。 |
| SystemIdle | 仅当系统空闲时才将工作项目调度到 UI 线程。这是实际得到处理的项目的最低优先级。 |
| ApplicationIdle | 仅当应用程序本身空闲时才将工作项目调度到 UI 线程。 |
| ContextIdle | 仅在优先级更高的工作项目得到处理后才将工作项目调度到 UI 线程。 |
| 后台 | 在所有布局、呈现和输入项目都得到处理后才将工作项目调度到 UI 线程。 |
| 输入 | 以与用户输入相同的优先级将工作项目调度到 UI 线程。 |
| 已加载 | 在所有布局和呈现都完成后才将工作项目调度到 UI 线程。 |
| 呈现 | 以与呈现引擎相同的优先级将工作项目调度到 UI 线程。 |
| DataBind | 以与数据绑定相同的优先级将工作项目调度到 UI 线程。 |
| 正常 | 以正常优先级将工作项目调度到 UI 线程。这是调度大多数应用程序工作项目时的优先级。 |
| 发送 | 以最高优先级将工作项目调度到 UI 线程。 |
在 WPF 中使用 BackgroundWorker
BackgroundWorker _backgroundWorker = new BackgroundWorker();
...
// Set up the Background Worker Events
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted +=
_backgroundWorker_RunWorkerCompleted;
// Run the Background Worker
_backgroundWorker.RunWorkerAsync(5000);
...
// Worker Method
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Do something
}
// Completed Method
void _backgroundWorker_RunWorkerCompleted(
object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
statusText.Text = "Cancelled";
}
else if (e.Error != null)
{
statusText.Text = "Exception Thrown";
}
else
{
statusText.Text = "Completed";
}
}
运行中的 DispatcherTimer 类
// Create a Timer with a Normal Priority
_timer = new DispatcherTimer();
// Set the Interval to 2 seconds
_timer.Interval = TimeSpan.FromMilliseconds(2000);
// Set the callback to just show the time ticking away
// NOTE: We are using a control so this has to run on
// the UI thread
_timer.Tick += new EventHandler(delegate(object s, EventArgs a)
{
statusText.Text = string.Format(
"Timer Ticked: {0}ms", Environment.TickCount);
});
// Start the timer
_timer.Start();