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


                        WPF线程,使用调度程序构建反应速度更快的应用程序
                         by:Shawn Wildermuth
                         From:http://msdn.microsoft.com/msdnmag/issues/07/10/WPFThreading/default.aspx?loc=zh#

若是您在建立一个直观、天然甚至精美的界面上花费了数月时间,但结果是用户不得不在他们的组合办公桌上敲打着手指等待程序响应,这会让人以为丢脸。因为长时间运行的进程致使应用程序的屏幕停滞不动,看到这样的状况是一件痛苦的事情。然而,建立响应迅速的应用程序须要进行认真的规划,这一般须要使长时间运行的进程在其余线程中工做,以便释放出 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 线程),全部用户界面工做均以其为通道。post

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

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

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

Back to top


DispatcherObject
spa

在 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

Fire 2 使用 VerifyAccess 与 CheckAccess
public class MyWpfObject : DispatcherObject
{
    public void DoSomething()       
    {
        VerifyAccess();

        // Do some work
    }

    public void DoSomethingElse()
    {
        if (CheckAccess())
        {
            // Something, only if called 
            // on the right thread
        }
    }
}


图3
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();


图4 Figure 4
 更新 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();

图5 Figure 5
异步更新 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();


图6 Figure 6
 DispatchPriority 优先级别(按优先级次序)


优先级 说明
非活动 工做项目已排队但未处理。
SystemIdle 仅当系统空闲时才将工做项目调度到 UI 线程。这是实际获得处理的项目的最低优先级。
ApplicationIdle 仅当应用程序自己空闲时才将工做项目调度到 UI 线程。
ContextIdle 仅在优先级更高的工做项目获得处理后才将工做项目调度到 UI 线程。
后台 在全部布局、呈现和输入项目都获得处理后才将工做项目调度到 UI 线程。
输入 以与用户输入相同的优先级将工做项目调度到 UI 线程。
已加载 在全部布局和呈现都完成后才将工做项目调度到 UI 线程。
呈现 以与呈现引擎相同的优先级将工做项目调度到 UI 线程。
DataBind 以与数据绑定相同的优先级将工做项目调度到 UI 线程。
正常 以正常优先级将工做项目调度到 UI 线程。这是调度大多数应用程序工做项目时的优先级。
发送 以最高优先级将工做项目调度到 UI 线程。

图7 Figure 7
在 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";
    }
}


图8 Figure 8
运行中的 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();