執行緒模型
更新:2007 年 11 月
Windows Presentation Foundation (WPF) 專為省去開發人員處理執行緒的困擾而設計,因此大部分的 WPF 開發人員都不需要撰寫使用超過一個執行緒的介面。由於多執行緒程式複雜又難以偵錯,因此在有單一執行緒方案時最好能避免使用多執行緒程式。
但是,不論架構多麼完備,任何 UI 架構都無法為每種問題提供單一執行緒方案。雖然 WPF 幾乎可達到這種理想,在有些情況中卻還是需要多執行緒才能改善使用者介面 (UI) 回應或應用程式效能。本文件會先討論一些背景資料,接著探討部分上述情況,再就一些較低階的細節問題進行討論,做為總結。
這個主題包含下列章節。
- 概觀和發送器
- 執行中的執行緒:範例
- 技術詳細資料和困難點
- 相關主題
概觀和發送器
一般而言,WPF 應用程式開始時會使用兩個執行緒:一個用於處理呈現,而另一個則用於管理 UI。呈現執行緒可有效地以隱藏方式在背景執行,而 UI 執行緒則會接收輸入、處理事件、繪製螢幕以及執行應用程式程式碼。大部分應用程式都使用單一 UI 執行緒,但是在某些情況下,最好使用多個執行緒。我們稍後會使用範例來討論此一狀況。
UI 執行緒會將工作項目佇列於 Dispatcher 物件中。Dispatcher 會依優先權選取工作項目,並逐一執行以完成每個工作。每個 UI 執行緒至少必須有一個 Dispatcher,而每個 Dispatcher 可以在單一執行緒中執行工作項目。
建置具回應且使用者易用應用程式的訣竅,是盡可能將工作項目保持最小,將 Dispatcher 輸送量提高到最大。使用這個方式,項目絕不會在 Dispatcher 佇列中等待處理太久而過時。輸入與回應之間的任何可感知延遲,都會讓使用者感到挫折。
那麼,WPF 應用程式會如何處理大型作業呢?如果程式碼牽涉到大量計算或需要查詢某個遠端伺服器上的資料庫,會如何處理?通常,答案是使用個別執行緒來處理大型作業,讓 UI 執行緒有空處理 Dispatcher 佇列中的項目。當大型作業完成時,會將結果回報給 UI 執行緒以供顯示。
以往,Windows 只允許建立 UI 項目的執行緒存取該項目。這表示,負責某個長時間執行工作的背景執行緒,在其完成時無法更新文字方塊。Windows 做這樣的處理,是要確保 UI 元件的完整性。如果由背景執行緒在繪製期間更新清單方塊的內容,則清單方塊可能會看起來很奇怪。
WPF 具有強制執行這項協調的內建互斥機制。WPF 中的大部分類別是衍生自 DispatcherObject。在建構時,DispatcherObject 會儲存連結至目前執行中執行緒之 Dispatcher 的參考。事實上,DispatcherObject 會與建立它的執行緒產生關聯。在程式執行期間,DispatcherObject 可以呼叫它的公用 VerifyAccess 方法。VerifyAccess 會檢查與目前執行緒相關聯的 Dispatcher,並將它與建構期間所儲存的 Dispatcher 參考進行比較。如果不相符,則 VerifyAccess 會擲回例外狀況 (Exception)。而 VerifyAccess 被呼叫的時機是在每個屬於 DispatcherObject 的方法開始時。
如果只有一個執行緒可以修改 UI,則背景執行緒如何與使用者互動?背景執行緒可以要求 UI 執行緒代表該背景執行緒執行作業。而方式是使用 UI 執行緒的 Dispatcher 註冊工作項目。Dispatcher 類別提供兩種方法註冊工作項目:Invoke 和 BeginInvoke。這兩種方法都會排定委派來執行。Invoke 是同步呼叫,也就是說,除非 UI 執行緒實際完成委派的執行,否則不會返回。BeginInvoke 是非同步的,會立即返回。
Dispatcher 會依優先權排列其佇列中項目的順序。在將項目加入至 Dispatcher 佇列時,可以指定十個層級。這些優先權是在 DispatcherPriority 列舉型別中進行維護。在 Windows SDK 文件中可以找到 DispatcherPriority 層級的詳細資訊。
執行中的執行緒:範例
單一執行緒應用程式與長期執行的計算
大部分圖形使用者介面 (GUI) 在等待為回應使用者互動而產生的事件時,大部分的時間會處於閒置狀態。只要小心進行程式設計,就能有效利用這些閒置時間,而不致影響 UI 的回應。WPF 執行緒模型不允許輸入作業中斷 UI 執行緒中進行的作業。這表示必須要定期返回 Dispatcher,以在暫止輸入事件過時之前處理這些事件。
參考下列範例:
這個簡單應用程式會從三開始往上計算,以搜尋質數。使用者按一下 [開始] 按鈕時,就會開始搜尋。當程式找到質數時,會使用找到的項目來更新使用者介面。而使用者隨時可以停止搜尋。
雖然十分簡單,但是質數搜尋可以永遠進行下去,因而產生部分困難。如果我們在按鈕的按一下事件處理常式內處理整個搜尋,則 UI 執行緒將永遠沒有機會處理其他事件。如此一來,UI 會無法回應輸入或處理訊息,因此不會重新繪製,也絕不會回應按鈕的按一下動作。
質數搜尋可以在個別執行緒中進行,但需要處理同步處理的問題。而使用單一執行緒方式,可以直接更新列出所找到最大質數的標籤。
如果將計算工作細分成可管理的區塊 (Chunk),則可以定期返回 Dispatcher 並處理事件。因此讓 WPF 有機會重新繪製並處理輸入。
分割計算與事件處理間之處理時間的最佳方式,是從 Dispatcher 管理計算。使用 BeginInvoke 方法,可以排定在繪製 UI 事件的相同佇列中進行質數檢查。在範例中,我們一次只排定一項質數檢查。質數檢查完成之後,會立即排定下一次的檢查。只有在處理暫止 UI 事件之後,才會繼續進行這項檢查。
Microsoft Word 會使用這個機制完成拼字檢查。拼字檢查是使用 UI 執行緒的閒置時間在背景完成。請查看程式碼。
下列範例顯示建立使用者介面的 XAML。
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="260" Height="75"
>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
<Button Content="Start"
Click="StartOrStop"
Name="startStopButton"
Margin="5,0,5,0"
/>
<TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
</StackPanel>
</Window>
下列範例顯示程式碼後置 (Code-Behind)。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public delegate void NextPrimeDelegate();
//Current number to check
private long num = 3;
private bool continueCalculating = false;
public Window1() : base()
{
InitializeComponent();
}
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to ture.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
}
}
下列範例顯示 Button 的事件處理常式。
private void StartOrStop(object sender, EventArgs e)
{
if (continueCalculating)
{
continueCalculating = false;
startStopButton.Content = "Resume";
}
else
{
continueCalculating = true;
startStopButton.Content = "Stop";
startStopButton.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new NextPrimeDelegate(CheckNextNumber));
}
}
除了更新 Button 上的文字之外,這個處理常式還會負責將委派加入至 Dispatcher 佇列,以排定第一次質數檢查。有時,這個事件處理常式完成其工作之後,Dispatcher 會選取這個委派來執行。
如先前所述,BeginInvoke 是用來排定委派以執行的 Dispatcher 成員。在此情況下,請選擇 SystemIdle 優先權。只有在沒有需要處理的重要事件時,Dispatcher 才會執行這個委派。UI 回應比數字檢查重要。我們也會傳遞新的委派,用以表示數字檢查常式。
public void CheckNextNumber()
{
// Reset flag.
NotAPrime = false;
for (long i = 3; i <= Math.Sqrt(num); i++)
{
if (num % i == 0)
{
// Set not a prime flag to ture.
NotAPrime = true;
break;
}
}
// If a prime number.
if (!NotAPrime)
{
bigPrime.Text = num.ToString();
}
num += 2;
if (continueCalculating)
{
startStopButton.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.SystemIdle,
new NextPrimeDelegate(this.CheckNextNumber));
}
}
private bool NotAPrime = false;
這個方法會檢查下一個奇數是否為質數。如果是質數,則方法會直接更新 bigPrime TextBlock 以反映它所找到的項目。因為計算是發生在用來建立該元件的相同執行緒中,所以可以做這樣的處理。若選擇使用另一個執行緒進行計算,則必須使用更複雜的同步處理機制,並在 UI 執行緒中執行更新。下面將示範這個狀況。
如需這個範例的完整原始程式碼,請參閱單一執行緒應用程式與長期執行的計算範例。
使用背景執行緒處理封鎖作業
在圖形應用程式中處理封鎖作業十分困難。因為應用程式看起來會像凍結,所以請不要從事件處理常式呼叫封鎖方法。雖然這些作業可以用個別的執行緒處理,但是完成時,必須與 UI 執行緒進行同步處理,原因是無法從背景工作執行緒 (Worker Thread) 直接修改 GUI。我們可以使用 Invoke 或 BeginInvoke,將委派插入至 UI 執行緒的 Dispatcher。最後,會使用修改 UI 項目的權限來執行這些委派。
在這個範例中,我們會模擬用來擷取天氣預報的遠端程序呼叫。而且使用個別背景工作執行緒執行這個呼叫,並在完成時於 UI 執行緒的 Dispatcher 中排定更新方法。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
// Delegates to be used in placking jobs onto the Dispatcher.
private delegate void NoArgDelegate();
private delegate void OneArgDelegate(String arg);
// Storyboards for the animations.
private Storyboard showClockFaceStoryboard;
private Storyboard hideClockFaceStoryboard;
private Storyboard showWeatherImageStoryboard;
private Storyboard hideWeatherImageStoryboard;
public Window1(): base()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Load the storyboard resources.
showClockFaceStoryboard =
(Storyboard)this.Resources["ShowClockFaceStoryboard"];
hideClockFaceStoryboard =
(Storyboard)this.Resources["HideClockFaceStoryboard"];
showWeatherImageStoryboard =
(Storyboard)this.Resources["ShowWeatherImageStoryboard"];
hideWeatherImageStoryboard =
(Storyboard)this.Resources["HideWeatherImageStoryboard"];
}
private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
hideWeatherImageStoryboard.Begin(this);
// Start fetching the weather forecast asynchronously.
NoArgDelegate fetcher = new NoArgDelegate(
this.FetchWeatherFromServer);
fetcher.BeginInvoke(null, null);
}
private void FetchWeatherFromServer()
{
// Simulate the delay from network access.
Thread.Sleep(4000);
// Tried and true method for weather forecasting - random numbers.
Random rand = new Random();
String weather;
if (rand.Next(2) == 0)
{
weather = "rainy";
}
else
{
weather = "sunny";
}
// Schedule the update function in the UI thread.
tomorrowsWeather.Dispatcher.BeginInvoke(
System.Windows.Threading.DispatcherPriority.Normal,
new OneArgDelegate(UpdateUserInterface),
weather);
}
private void UpdateUserInterface(String weather)
{
//Set the weather image
if (weather == "sunny")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"SunnyImageSource"];
}
else if (weather == "rainy")
{
weatherIndicatorImage.Source = (ImageSource)this.Resources[
"RainingImageSource"];
}
//Stop clock animation
showClockFaceStoryboard.Stop(this);
hideClockFaceStoryboard.Begin(this);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private void HideClockFaceStoryboard_Completed(object sender,
EventArgs args)
{
showWeatherImageStoryboard.Begin(this);
}
private void HideWeatherImageStoryboard_Completed(object sender,
EventArgs args)
{
showClockFaceStoryboard.Begin(this, true);
}
}
}
下列是要注意的一些詳細資料。
建立按鈕處理常式
private void ForecastButtonHandler(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; hideWeatherImageStoryboard.Begin(this); // Start fetching the weather forecast asynchronously. NoArgDelegate fetcher = new NoArgDelegate( this.FetchWeatherFromServer); fetcher.BeginInvoke(null, null); }
按一下按鈕時,會顯示時鐘圖案,並開始進行它的動畫。我們停用了按鈕。並在新執行緒中叫用 (Invoke) FetchWeatherFromServer 方法,然後返回,讓 Dispatcher 在我們等待收集天氣預報時處理事件。
擷取天氣
private void FetchWeatherFromServer() { // Simulate the delay from network access. Thread.Sleep(4000); // Tried and true method for weather forecasting - random numbers. Random rand = new Random(); String weather; if (rand.Next(2) == 0) { weather = "rainy"; } else { weather = "sunny"; } // Schedule the update function in the UI thread. tomorrowsWeather.Dispatcher.BeginInvoke( System.Windows.Threading.DispatcherPriority.Normal, new OneArgDelegate(UpdateUserInterface), weather); }
為了簡化,在這個範例中實際上並沒有任何網路程式碼。而是讓新執行緒進入四秒的休眠時間,以模擬網路存取的延遲。此時,原始 UI 執行緒仍然會執行,並回應事件。為了顯示這個情況,我們會讓動畫持續執行,而且最小化和最大化按鈕也會繼續運作。
更新 UI
private void UpdateUserInterface(String weather) { //Set the weather image if (weather == "sunny") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "SunnyImageSource"]; } else if (weather == "rainy") { weatherIndicatorImage.Source = (ImageSource)this.Resources[ "RainingImageSource"]; } //Stop clock animation showClockFaceStoryboard.Stop(this); hideClockFaceStoryboard.Begin(this); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
當 UI 執行緒中的 Dispatcher 有時間時,會執行排定的 UpdateUserInterface 呼叫。這個方法會停止時鐘動畫,並選擇影像以說明天氣,然後顯示該影像,並還原 "fetch forecast" 按鈕。
如需這個範例的完整原始程式碼,請參閱透過發送器模擬氣象服務範例。
多視窗多執行緒
部分 WPF 應用程式需要多個最上層視窗 (Top-Level Window)。最適當的是使用一個 Thread/Dispatcher 組合來管理多個視窗,但是有時使用多個執行緒的效果會更好。這特別適用於其中一個視窗可能會獨佔執行緒時。
Windows 檔案總管就是以這種形式運作。每個新檔案總管視窗都屬於原始處理序,但是在獨立執行緒的控制下建立。
使用 WPF Frame 控制項,可以顯示網頁,因此可以輕鬆地建立簡單的 Internet Explorer 替代。我們一開始會使用開啟新檔案總管視窗的重要功能。當使用者按一下 [開新視窗] 按鈕時,會在個別執行緒中啟動視窗的複本。如此一來,視窗之一中的長期執行或封鎖作業就不會鎖定其他的所有視窗。
實際上,Web 瀏覽器模型有它自己的複雜執行緒模型。由於它應該熟悉大部分的讀取器 (Reader),所以我們選擇使用它。
下列範例顯示程式碼。
<Window x:Class="SDKSamples.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MultiBrowse"
Height="600"
Width="800"
Loaded="OnLoaded"
>
<StackPanel Name="Stack" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button Content="New Window"
Click="NewWindowHandler" />
<TextBox Name="newLocation"
Width="500" />
<Button Content="GO!"
Click="Browse" />
</StackPanel>
<Frame Name="placeHolder"
Width="800"
Height="550"></Frame>
</StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
{
public partial class Window1 : Window
{
public Window1() : base()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri("https://www.msn.com");
}
private void Browse(object sender, RoutedEventArgs e)
{
placeHolder.Source = new Uri(newLocation.Text);
}
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
}
}
在這段內容中,最需要注意這個程式碼的下列執行緒區段:
private void NewWindowHandler(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
按一下 [開新視窗] 按鈕時會呼叫這個方法。這個方法會建立新的執行緒,並以非同步方式啟動。
private void ThreadStartingPoint()
{
Window1 tempWindow = new Window1();
tempWindow.Show();
System.Windows.Threading.Dispatcher.Run();
}
這個方法是新執行緒的起點。新視窗是在這個執行緒的控制下建立。WPF 會自動建立新的 Dispatcher,以管理新的執行緒。若要讓視窗運作,請啟動 Dispatcher。
如需這個範例的完整原始程式碼,請參閱多執行緒 Web 瀏覽器範例。
技術詳細資料和困難點
使用執行緒撰寫元件
《Microsoft .NET Framework 開發人員手冊》說明元件如何將非同步行為公開給其用戶端的模式 (請參閱事件架構非同步模式概觀)。例如,假設我們要將 FetchWeatherFromServer 方法封裝 (Package) 至可重複使用的非圖形元件中。這項作業遵循標準 Microsoft .NET Framework 模式,會與下方所示類似。
public class WeatherComponent : Component
{
//gets weather: Synchronous
public string GetWeather()
{
string weather = "";
//predict the weather
return weather;
}
//get weather: Asynchronous
public void GetWeatherAsync()
{
//get the weather
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}
public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
public GetWeatherCompletedEventArgs(Exception error, bool canceled,
object userState, string weather)
:
base(error, canceled, userState)
{
_weather = weather;
}
public string Weather
{
get { return _weather; }
}
private string _weather;
}
public delegate void GetWeatherCompletedEventHandler(object sender,
GetWeatherCompletedEventArgs e);
GetWeatherAsync 會使用先前說明的其中一個技術 (如建立背景執行緒) 以非同步方式進行工作,而不是封鎖呼叫執行緒。
這個模式的其中一個重要部分,是在呼叫 MethodNameAsync 方法以開始的相同執行緒上,呼叫 MethodNameCompleted 方法。雖然透過儲存 CurrentDispatcher,可以使用 WPF 輕鬆地完成這個作業,但是非圖形元件只能用於 WPF 應用程式,卻不能用於 Windows Form 或 ASP.NET 程式。
DispatcherSynchronizationContext 類別可以解決這個需求,請將它視為 Dispatcher 的簡化版本,可與其他 UI 架構搭配運作。
public class WeatherComponent2 : Component
{
public string GetWeather()
{
return fetchWeatherFromServer();
}
private DispatcherSynchronizationContext requestingContext = null;
public void GetWeatherAsync()
{
if (requestingContext != null)
throw new InvalidOperationException("This component can only handle 1 async request at a time");
requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;
NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);
// Launch thread
fetcher.BeginInvoke(null, null);
}
private void RaiseEvent(GetWeatherCompletedEventArgs e)
{
if (GetWeatherCompleted != null)
GetWeatherCompleted(this, e);
}
private string fetchWeatherFromServer()
{
// do stuff
string weather = "";
GetWeatherCompletedEventArgs e =
new GetWeatherCompletedEventArgs(null, false, null, weather);
SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
requestingContext.Post(callback, e);
requestingContext = null;
return e.Weather;
}
private void DoEvent(object e)
{
//do stuff
}
public event GetWeatherCompletedEventHandler GetWeatherCompleted;
public delegate string NoArgDelegate();
}
巢狀提取
有時,並不適合完全鎖定 UI 執行緒。請考慮使用 MessageBox 類別的 Show 方法。除非使用者按一下 [確定] 按鈕,否則 Show 不會返回,但會建立必須具有訊息迴圈的視窗,以進行互動。在等待使用者按一下 [確定] 時,原始應用程式視窗不會回應使用者輸入,但是會繼續處理繪製訊息。原始視窗會在涵蓋及顯示時自行重新繪製。
部分執行緒必須負責訊息方塊視窗。WPF 可以針對訊息方塊視窗建立新執行緒,但是這個執行緒無法繪製原始視窗中的已停用項目 (請記住先前討論過的互斥)。而 WPF 是使用巢狀訊息處理系統。Dispatcher 類別包括稱為 PushFrame 的特殊方法,此方法可儲存應用程式目前的執行點,然後開始新的訊息迴圈。當巢狀訊息迴圈完成時,會在原始 PushFrame 呼叫之後繼續執行。
在此情況下,PushFrame 會在呼叫 MessageBox.Show 時維護程式內容,並開始新訊息迴圈以重新繪製背景視窗,以及處理訊息方塊視窗的輸入。當使用者按一下 [確定] 並清除快顯視窗 (Pop-Up Window) 時,巢狀迴圈會結束,以及在呼叫 Show 之後繼續控制。
過時路由事件
WPF 中的路由事件系統會在引發事件時通知整個樹狀目錄。
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
在橢圓形上按下滑鼠左鍵時,會執行 handler2。handler2 完成之後,會將事件傳遞給 Canvas 物件,這個物件會使用 handler1 來進行處理。只有在 handler2 未明確將事件物件標記為已處理時,才會發生這種情況。
handler2 可能會使用大量的時間來處理這個事件。handler2 可能也會使用 PushFrame 來開始數小時不會返回的巢狀訊息迴圈。在這個訊息迴圈完成時,如果 handler2 未將事件標記為已處理,則事件即使已太舊還是會在樹狀目錄中傳遞。
重新進入和鎖定
Common Language Runtime (CLR) 的鎖定機制不會如預期進行;可能有人期望執行緒會在要求鎖定時完全停止作業。實際上,執行緒會繼續接收和處理高優先權訊息。這有助於防止死結 (Deadlock),並讓介面做出最低程度的回應,但是可能會造成微小的問題。在大部分情況下,您並不需要知道這種狀況,但在極少數的情況中 (通常是與 Win32 視窗訊息或 COM STA 元件有關),就必須進行了解。
因為開發人員工作時都假設不會有一個以上的執行緒存取 UI,所以大部分介面在建置時並未考慮到執行緒安全。在此情況下,該單一執行緒可能會在未預期的時間進行環境變更,而造成應由 DispatcherObject 互斥機制解決的不佳效果。請考慮使用下列虛擬程式碼:
在大部分的情況下這個方法都適用,但是在 WPF 中,有時這類未預期重新進入實際上會造成問題。因此,在特定重要時間,WPF 會呼叫 DisableProcessing,將該執行緒的鎖定指示變更為使用 WPF 無重新進入鎖定,而不是一般的 CLR 鎖定。
那麼,CLR 小組為何選用這個行為呢?這與 COM STA 物件及最終化執行緒有關。對物件進行記憶體回收時,物件的 Finalize 方法是在專用的完成項執行緒上執行,而不是在 UI 執行緒上執行。而因為在 UI 執行緒上建立的 COM STA 物件只可以在 UI 執行緒上進行處置 (Dispose),所以會發生問題。CLR 相當於 BeginInvoke (在此情況下,使用的是 Win32 的 SendMessage)。但是如果 UI 執行緒忙碌,便會停止完成項執行緒,而且無法處置 COM STA 物件,這會產生嚴重的記憶體遺漏 (Memory Leak)。所以,CLR 小組為了讓鎖定正常運行,做出了以上困難的決定。
WPF 的工作是要避免未預期的重新進入,同時不要重新引入記憶體遺漏,這就是為何不在所有位置都封鎖重新進入。