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