Windows Presentation Foundation (WPF) 旨在帮助开发人员解决线程的难题。这样,大多数 WPF 开发人员都不必编写使用多个线程的接口。由于多线程程序很复杂,且难以调试,因此只要存在单线程解决方案,就应避免使用多个线程。
但是,无论体系结构多么完善,没有任何 UI 框架能够为每一类问题提供单线程解决方案。WPF 接近这一理想,但是在某些情况下,仍然可通过采用多个线程来提高用户界面 (UI) 响应速度或应用程序性能。在讨论一些背景材料后,本文将探讨其中一些情况,最后从较低层次进行一些详细讨论。
本主题包括下列各节:
- 概述和调度程序
- 操作中的线程:示例
- 技术细节和难点
- 相关主题
概述和调度程序
通常,WPF 应用程序从两个线程开始:一个用于处理呈现,一个用于管理 UI。呈现线程有效地隐藏在后台运行,而 UI 线程则接收输入、处理事件、绘制屏幕以及运行应用程序代码。大多数应用程序都使用一个 UI 线程,但在某些情况下,最好使用多个线程。我们将在后面举例说明这一点。
UI 线程在一个名为
要构建响应速度快、且用户友好的应用程序,诀窍是减小工作项,以最大限度地提高
那么,WPF 应用程序应如何处理大型操作? 如果您的代码涉及大型计算,或者需要查询某台远程服务器上的数据库,应怎么办? 通常的办法是在单独的线程中处理大型操作,而专门让 UI 线程来处理
一直以来,Windows 只允许创建 UI 元素的线程访问这些元素。这意味着负责一些长时间运行任务的后台线程无法更新已完成的文本框。Windows 这样做是为了确保 UI 组件的完整性。如果列表框的内容在绘制过程中被后台线程更新,那么该列表框看上去将会很奇怪。
WPF 使用一种内置互斥机制来强制执行这种协调。WPF 中的大多数类都派生自
如果只有一个线程可以修改 UI,那么后台线程如何与用户交互? 后台线程可以请求 UI 线程代表它执行操作,这是通过向 UI 线程的
操作中的线程:示例
具有长时间运行计算的单线程应用程序
大多数图形用户界面 (GUI) 的大部分空闲时间都是因为等待响应用户交互而生成的事件而造成的。
. 通过仔细地编程,可以建设性地使用这一空闲时间,同时不影响 UI 的响应。WPF 线程模型不允许输入中断在 UI 线程中发生的操作。这意味着您必须定期返回到
请看下面的示例:
这是一个简单的应用程序,从 3 开始往上数,搜索质数。当用户单击“Start”(开始)按钮时,搜索开始。当程序找到一个质数时,则会用它的搜索结果更新用户界面。用户可以随时停止搜索。
尽管此应用程序非常简单,但质数搜索可以无限地继续下去,这带来了一定的困难。 如果在按钮的 click 事件处理程序中处理整个搜索,UI 线程将永远没有机会处理其他事件。UI 将无法响应输入或处理消息。它永远不会重新绘制,也永远不会响应按钮单击。
我们可以在一个单独的线程中执行质数搜索,但之后需要处理同步问题。使用单线程方法,可以直接更新列出找到的最大质数的标签。
如果将计算任务分成多个易管理的块,就可以定期返回到
在计算与事件处理之间划分处理时间的最佳方法是从
Microsoft Word 就是使用这一机制来实现拼写检查。拼写检查是利用 UI 线程的空闲时间在后台执行的。我们来看一看代码。
下面的示例演示创建用户界面的 XAML。
WPF | |
---|---|
<Window x:Class="SDKSamples.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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> |
下面的示例演示代码隐藏。
C# | |
---|---|
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(); } public 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; } } |
下面的示例演示
C# | |
---|---|
public 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)); } } |
除了更新
前面已提到,
C# | |
---|---|
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
有关此示例的完整源代码,请参见
用后台线程处理阻止操作
在图形应用程序中处理阻止操作很困难。我们不希望从事件处理程序中调用阻止方法,因为这样应用程序看上去好像已冻结。可以使用一个单独的线程来处理这些操作,但是完成后必须与 UI 线程同步,因为不能从辅助线程直接修改 GUI。可以使用
在本示例中,模拟检索天气预报的远程过程调用。使用一个单独的辅助线程来执行此调用,并在完成后在 UI 线程的
C# | |
---|---|
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); } } } |
下面给出了一些值得注意的细节。
-
创建按钮处理程序
C# 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); }
当单击按钮时,显示时钟图并开始显示它的动画效果。禁用该按钮,在一个新线程中调用 FetchWeatherFromServer 方法,然后返回,这样
-
获取天气预报
C# 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
C# 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 线程中的
有关此示例的完整源代码,请参见
多个窗口,多个线程
一些 WPF 应用程序需要多个高级别窗口。一个线程/
Windows 资源管理器就是以这种方式工作的。每个新的资源管理器窗口都属于原始进程,但是在独立线程的控制下创建的。
使用 WPF
事实上,Web 浏览器模型有它自己的复杂线程模型。我们选择它是因为大多数读者对它都很熟悉。
下面的示例演示代码。
XAML | |
---|---|
<Window x:Class="SDKSamples.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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> |
C# | |
---|---|
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("http://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(); } } } |
在本语境中,这些代码中的以下线程片段最有意义:
C# | |
---|---|
private void NewWindowHandler(object sender, RoutedEventArgs e) { Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint)); newWindowThread.SetApartmentState(ApartmentState.STA); newWindowThread.IsBackground = true; newWindowThread.Start(); } |
当单击“new window”(新建窗口)按钮时,调用此方法。它创建一个新线程并以异步方式启动它。
C# | |
---|---|
private void ThreadStartingPoint() { Window1 tempWindow = new Window1(); tempWindow.Show(); System.Windows.Threading.Dispatcher.Run(); } |
此方法是新线程的起点。我们在此线程的控制下创建一个新窗口。WPF 自动创建一个新的
有关此示例的完整源代码,请参见
技术细节和难点
使用线程编写组件
Microsoft .NET Framework Developer's Guide(《Microsoft .NET Framework 开发人员指南》)介绍了组件向客户端公开异步行为的一种模式(请参见
C# | |
---|---|
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 将使用前面介绍的一种技术(如创建后台线程)来异步执行工作,同时不阻止调用线程。
此模式的最重要部分之一是最初在调用方法名称 Async 方法的线程上调用方法名称 Completed 方法。通过存储
C# | |
---|---|
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 线程是不可行的。我们可以考虑
必须有一个线程负责消息框窗口。WPF 可以仅仅为该消息框窗口创建一个新线程,但此线程无法绘制原始窗口中被禁用的元素(请回忆前面关于互斥的讨论)。WPF 使用嵌套的消息处理系统。
在这种情况下,
失效的路由事件
当引发了事件时,WPF 中的路由事件系统会通知整个树。
XAML | |
---|---|
<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 结束后,事件将传递到
handler2 可能需要大量的时间来处理此事件。handler2 可以使用
重新进入和锁定
公共语言运行库 (CLR) 的锁定机制与大家想象的不完全一样;您可能想象一个线程在请求锁时会完全停止操作。事实上,该线程会继续接收和处理高优先级消息。这有助于防止死锁,并使界面作出尽可能小的响应,但也引入了发生小 Bug 的可能性。 在大多数情况下您不需要对此有任何了解,但在少数情况下(通常涉及 Win32 窗口消息或 COM STA 组件),需要对此加以注意。
大多数界面在构建时都并未考虑线程安全性,因为开发人员在开发时假定绝不会有一个以上的线程访问 UI。在这种情况下,单个线程可能会在意外的时间进行环境更改,从而导致一些不好的影响,但
大多数情况下这是正常的,但在 WPF 中,某些情况下这种意外重新进入确实会导致出现问题。因此,在某些关键时刻,WPF 调用
那么为什么 CLR 团队会选择这样的行为呢? 因为它必须处理 COM STA 对象和终止线程。当对对象进行垃圾回收时,会在专门的终结器线程(而不是 UI 线程)上运行它的 Finalize 方法。问题就在这里:在 UI 线程上创建的 COM STA 对象只能在 UI 线程上释放。CLR 执行
WPF 的任务是避免意外的重新进入,不重新引入内存泄漏。这就是我们在任何位置都不阻止重新进入的原因。
请参见MSDN
概念