WPF 进阶 多线程(Dispathcer DispatcherObject BackgroundWorker 类)
110 111 集 1.1h
WPF支持单线程单元(Sing唇readApartment)模型,该模型与在Windows窗体应
用程序中使用的模型非常类似。它具有以下几条核心规则:
WPF元素具有线程关联性(Thread affinity)。创建WPF元素的线程拥有所创建的元素,其
他线程不能直接与这些WPF元素进行交互(元素是在窗口中显示的WPF对象)。
具有线程关联性的WPF对象都在类层次的某个位置继承自DispatcherObject类。
DispatchelObJect类提供了少量成员,用于核实为了使用特定的对象,代码是否在正确的
线程上执行,并且(没有在正确的线程上执行)是否能切换位置。
实际上,线程运行整个应用程序并拥有所有WPF对象。尽管可使用单独的线程显示单独
的窗口,但这种设计很少硬用。
Dispatcher类
调度程序(dispatcher)管理在WPF应用程序中发生的操作。调度程序拥有应用程序线程,并
管理工作项队列。当应用程序运行时,调度程序接受新的工作请求,并且一次执行一个任务。
从技术角度看,当在新线第一次实例化Dispatcher0bject类的派生类时,会创建调度程
序。如果创建相互独立的线程,并用它们显示相互独立的窗口,最终将创建多个调度程序。然
而,大多数应用程序都保持简单方式,并坚持使用一个用户界面线程和一个调度程序。然后,
他们使用多线程管理数据操作和其他后台任务。
DispatcherObject类:
在大多情况下,不会直接与调度程序交互。但会花费大量时间使用DispatcherObJect类的
实例,因为个WPF可视化对象都继承自这个类。DispatcherObject实例是链接到调度程序的
简单对象一一换句话说,是绑定到调度程序线程的对象。
DispatcherObJect类只提供三个成员:
Dispatcher:返回管理该对象的调度程序
CheckAccess():如果代码在正确的线程上使用对象,就返回巛否则返回false
VeriftyAccess():如果代码在正确的线程上使用对象,就什么也不做,否则抛出InvalidOperauonException异常
WPF对象为保护自身会频繁调用VerifyAccess()方法。但这并不是说WPF对象会调用
VeriftyAccess()方法来响应每个操作(因为这样严重影响性能),但会足够频繁地调用该方法,从
而不可能在错误的线程中长时间使用一个对象。
private void cmdBreakRules_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(UpdateTextWrong); thread.Start(); } private void UpdateTextWrong() { txt.Text = "Here is some new text."; }
上面的代码注定会失败。UpdateTextWrong()方法将在新线程上执行,并且不允许这个新线
程访问WPF对象。TextBox对象通过调用VenfyAccess()方法捕获这一非法操作,
并抛出InvalidOperationException异常。
正确的做法是获取拥有TextBox对象的调度程序的引用(这个调度程序也拥有应
用程序中的窗口和所有其他WPF对象)。一旦访问这个调度程序,就可以调用DispatcherInvoke()
方法将一些代码封送到调度程序线程。本质上,Beginvoke()方法会将代码安排为调度程序的
任务。然后调度程序会执行这些代码。
private void cmdFollowRules_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(UpdateTextRight); thread.Start(); } private void UpdateTextRight() { Thread.Sleep(TimeSpan.FromSeconds(5)); this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart) delegate() { txt.Text = "Here is some new text."; } ); }
Dispatcher.BeginInvoke方法具有两个参数。
第一个参数指示任务的优先级。在大多数清况下,会使用DispatcherPnontyNormaI。
BegninInvoke()方法的第二个参数是指向一个方法的委托,该方法具有希望执行的代码。这
个方法可以旌代码中的其他地方定义,也可以使用匿名方法在内部定义代码(就像在这个示例中
所做的那样)。对于简单操作,使用内联方法效果较好,例如本例只需要使用一行代码更新用户。
请记在,如果正在执行耗时的后台工作,就需要在单独的线程中执行这个操作,然后将操作
结果封送到调度程序线程(在此更新用户界面或修改共享对象)。在传递给BeginInvoke()的方法中
执行耗时的代码是不合理的。
BackgroundWorker类:
BackgroundWorker组件是.NET2.0版本提供的,用于简化Windows窗体应用程序中与线程
相关的问题。然而,在WPF中同样使用BackgroundWorker组件。BackgroundWorker组件为在
单独线程中运行耗时的任务提供了一种非常简单的方法。它在后台使用调度程序,并使用基于
事件的模型对封送问题进行抽象。
BackgroundWorker组件还支持另外两个功能:进度(progess)事件和取消消息。
对于这两种情况都隐藏了线程细节,以方便代码的编写。
namespace Multithreading { public class Worker { public static int[] FindPrimes(int fromNumber, int toNumber) { return FindPrimes(fromNumber, toNumber, null); } public static int[] FindPrimes(int fromNumber, int toNumber, System.ComponentModel.BackgroundWorker backgroundWorker) { int[] list = new int[toNumber - fromNumber]; // Create an array containing all integers between the two specified numbers. for (int i = 0; i < list.Length; i++) { list[i] = fromNumber; fromNumber += 1; } //find out the module for each item in list, divided by each d, where //d is < or == to sqrt(to) //if the remainder is 0, the nubmer is a composite, and thus //we mark its position with 0 in the marks array, //otherwise the number is a prime, and thus mark it with 1 int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber)); int[] mark = new int[list.Length]; for (int i = 0; i < list.Length; i++) { for (int j = 2; j <= maxDiv; j++) { if ((list[i] != j) && (list[i] % j == 0)) { mark[i] = 1; } } int iteration = list.Length / 100; if ((i % iteration == 0) && (backgroundWorker != null)) { if (backgroundWorker.CancellationPending) { // Return without doing any more work. return null; } if (backgroundWorker.WorkerReportsProgress) { //float progress = ((float)(i + 1)) / list.Length * 100; backgroundWorker.ReportProgress(i / iteration); //(int)Math.Round(progress)); } } } //create new array that contains only the primes, and return that array int primes = 0; for (int i = 0; i < mark.Length; i++) { if (mark[i] == 0) primes += 1; } int[] ret = new int[primes]; int curs = 0; for (int i = 0; i < mark.Length; i++) { if (mark[i] == 0) { ret[curs] = list[i]; curs += 1; } } if (backgroundWorker != null && backgroundWorker.WorkerReportsProgress) { backgroundWorker.ReportProgress(100); } return ret; } } }
为使用BackgroundWorker,首先需要创建该类的一个实例。创建该实例的两种选择:
1.可在代码中创建BackgroundWorker对象,并用代码关联所有事件处理程序。
2.可在XAML中声明BackgroundWorker对象。这种方法的优点是可使用特性关联事件处
理程序。因为BackgroundWorder组件不是可见的WPF元素,所以不能在任意位置放置。
需要作为窗口的资源声明BackgroundWorker对象。
这两种方法是等效的。下载示例使用的是第二种方法。第一步是通过名称空间导入,
xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
<Window.Resources> <cm:BackgroundWorker x:Key="backgroundWorker" WorkerReportsProgress="True" WorkerSupportsCancellation="True" DoWork="backgroundWorker_DoWork" ProgressChanged="backgroundWorker_ProgressChanged" RunWorkerCompleted="backgroundWorker_RunWorkerCompleted"></cm:BackgroundWorker> </Window.Resources>
在标记的Windows.Resources部分声明BackgroundWorker对象的优点是,可使用特性设置
其属性并关联事件处理程序。例如,上面是该例最后使用的BackgroundWorker标签,该标签支持
进度通知和取消操作,并为DoWork事件、ProgressChanged事件以及RunWorkerCompleted事
件关联了事件处理程序:
using System; using System.Collections.Generic; using System.Text; namespace Multithreading { public class FindPrimesInput { //在素数查找示例中,使用BackgroundWorker组件的第一步是创建一个自定义类,通过这个 //自定义类向BackgroundWorker对象传递输入参数。当调用BackgroundWorker.RunWorkerAsyn«) //方法时,可提供任何对象,相应的对象将被传递到DoWork事件。但只能提供一个对象,所以 //需要将结束数字和开始数字封装到一个类中,如下所示: public int To { get; set; } public int From { get; set; } public FindPrimesInput(int from, int to) { To = to; From = from; } } }
namespace Multithreading { public class Worker { public static int[] FindPrimes(int fromNumber, int toNumber) { return FindPrimes(fromNumber, toNumber, null); } public static int[] FindPrimes(int fromNumber, int toNumber, System.ComponentModel.BackgroundWorker backgroundWorker) { int[] list = new int[toNumber - fromNumber]; // Create an array containing all integers between the two specified numbers. for (int i = 0; i < list.Length; i++) { list[i] = fromNumber; fromNumber += 1; } //find out the module for each item in list, divided by each d, where //d is < or == to sqrt(to) //if the remainder is 0, the nubmer is a composite, and thus //we mark its position with 0 in the marks array, //otherwise the number is a prime, and thus mark it with 1 int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber)); int[] mark = new int[list.Length]; for (int i = 0; i < list.Length; i++) { for (int j = 2; j <= maxDiv; j++) { if ((list[i] != j) && (list[i] % j == 0)) { mark[i] = 1; } } //FindPrimes()方法每完成1%的工作就报告一次,使用的代码如下所示: int iteration = list.Length / 100; if ((i % iteration == 0) && (backgroundWorker != null)) { //当调用CancelAsync()方法时不会自动发生任何操作。相反,执行任务的代码需要显式地检 //查取消请求,执行所有清除操作,然后返回。下面的FindPnmes()方法中的代码在即将报告进 //度之前检查取消请求: if (backgroundWorker.CancellationPending) { // Return without doing any more work. return null; } if (backgroundWorker.WorkerReportsProgress) { //float progress = ((float)(i + 1)) / list.Length * 100; backgroundWorker.ReportProgress(i / iteration); //(int)Math.Round(progress)); } } } //create new array that contains only the primes, and return that array int primes = 0; for (int i = 0; i < mark.Length; i++) { if (mark[i] == 0) primes += 1; } int[] ret = new int[primes]; int curs = 0; for (int i = 0; i < mark.Length; i++) { if (mark[i] == 0) { ret[curs] = list[i]; curs += 1; } } if (backgroundWorker != null && backgroundWorker.WorkerReportsProgress) { backgroundWorker.ReportProgress(100); } return ret; } } }
using System; using System.Collections.Generic; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.ComponentModel; namespace Multithreading { /// <summary> /// Interaction logic for BackgroundWorkerTest.xaml /// </summary> public partial class BackgroundWorkerTest : System.Windows.Window { public BackgroundWorkerTest() { InitializeComponent(); //为在代码中访问该资源,需要将它从Resources集合中提取出来。在本例中,窗口在其构造 //函数中执行该步骤,使所有事件处理代码都能更容易地访问该资源: backgroundWorker = ((BackgroundWorker)this.FindResource("backgroundWorker")); } private BackgroundWorker backgroundWorker; /// <summary> /// 为运行BackgroundWorker对象,需要调用BackgroundWorker.RunWorkerAsync()方法,并 /// 传入FindPnmes1nput对象。当用户单击FindPrimes按钮时,下面的代码将完成这一工作: /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void cmdFind_Click(object sender, RoutedEventArgs e) { // Disable the button and clear previous results. cmdFind.IsEnabled = false; cmdCancel.IsEnabled = true; lstPrimes.Items.Clear(); // Get the search range. int from, to; if (!Int32.TryParse(txtFrom.Text, out from)) { MessageBox.Show("Invalid From value."); return; } if (!Int32.TryParse(txtTo.Text, out to)) { MessageBox.Show("Invalid To value."); return; } // Start the search for primes on another thread. FindPrimesInput input = new FindPrimesInput(from, to); backgroundWorker.RunWorkerAsync(input); } /// <summary> /// 当BackgroundWorker对象开始执行后,从CLR线程池提取一个自由线程,然后从这个线 /// 程引发DoWork事件。您可以处理DoWork事件并开始执行耗时的任务,但切勿访问共享数据(如 ///窗口类中的字段)或用户界面对象。一旦完成工作,对象就会引发RunWorker- ///Completed事件以通知应用程序。这个事件在调度程序线程引发,在该线程上您可以访问共享 ///数据和用户界面,而不会导致任何问题。 ///一旦BackgroundWorker对象请求到线程,就引发DoWork事件。可通过处理这个事件来调 /// 用Worker.FindPrimes()方法。DoWork事件提供一个DoWorkEventArgs对象,该对象是检索和 ///返回信息的要素。可通过DoWorkEventArgs.Argument属性检索输入对象,并通过设置DoWork- ///EventArgs、Resu1t属性返回结果。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { // Get the input values. FindPrimesInput input = (FindPrimesInput)e.Argument; // Start the search for primes and wait. int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker); //DoWork事件处理程序中的代码还需要显式地将DoWorkEventArgs℃ancel属性设置为e, //以完成取消操作。然后用户可以从方法中返回,而不必试图构建素数字符串。 if (backgroundWorker.CancellationPending) { e.Cancel = true; return; } // Return the result. e.Result = primes; } /// <summary> /// 一旦Worker.FindPrimes()方法执行完毕,BackgroundWorker对象就在调度程序线程引发 /// RunWorkerCompletedEventArgs事件。这时,可通过RunWorkerCompIetedEventArgs.ResuIt属性 /// 检索结果。然后可更新界面并访问窗口级别的变量,而不必担心任何问题。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { //DoWork事件处理程序中的代码还需要显式地将DoWorkEventArgs℃ancel属性设置为true //以完成取消操作。然后用户可以从方法中返回,而不必试图构建素数字符串。 if (e.Cancelled) { MessageBox.Show("Search cancelled."); } else if (e.Error != null) { // An error was thrown by the DoWork event handler. MessageBox.Show(e.Error.Message, "An Error Occurred"); } else { int[] primes = (int[])e.Result; foreach (int prime in primes) { lstPrimes.Items.Add(prime); } } cmdFind.IsEnabled = true; cmdCancel.IsEnabled = false; progressBar.Value = 0; } /// <summary> /// BackgroundWorker类还为跟踪进度提供了内置支持。在长时间运行的任务中,如果需要持 ///续向客户端发送己经完成的工作量的信息,这一支持是很有用的。 ///要为进度添加支持,首先需要将BackgroundWorker.WorkerReportsProgress属性设置为true。 ///实际上,提供和显示执行进度信息是两个步骤。首先,DoWork事件处理代码需要调用 /// BackgroundWorker.ReportProgress()方法,并提供己经完成的百分比(从0%到100%)。可根据个 ///人喜好或多或少地执行该工作。每次调用R印proess() 方法时,BackgroundWorker对象都会 ///Changed事件是从用户界面线程引发的,所以不需要使用Dispatcher.Begin1nvoke()方法。 ///FindPnmes() 方法每完成1%的工作就报告一次,使用的代码如下所示: /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar.Value = e.ProgressPercentage; } /// <summary> /// 使用BackgroundWorker对象为长时间执行的任务添加取消支持同样也很容易。第一步是 /// 将BackgroundWorker.WorkerSuppoflsCanceIIation属性设置为恤re。 /// 为请求取消,代码需要调用BackgroundWorker℃ancelAsync()方法。在该例中,当单击cancel /// 按钮时请求取消: /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void cmdCancel_Click(object sender, RoutedEventArgs e) { backgroundWorker.CancelAsync(); } } }
<Window x:Class="Multithreading.BackgroundWorkerTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Multithreading" Height="323.2" Width="305.6" xmlns:cm="clr-namespace:System.ComponentModel;assembly=System" > <Window.Resources> <cm:BackgroundWorker x:Key="backgroundWorker" WorkerReportsProgress="True" WorkerSupportsCancellation="True" DoWork="backgroundWorker_DoWork" ProgressChanged="backgroundWorker_ProgressChanged" RunWorkerCompleted="backgroundWorker_RunWorkerCompleted"></cm:BackgroundWorker> </Window.Resources> <Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock Margin="5">From:</TextBlock> <TextBox Name="txtFrom" Grid.Column="1" Margin="5">1</TextBox> <TextBlock Grid.Row="1" Margin="5">To:</TextBlock> <TextBox Name="txtTo" Grid.Row="1" Grid.Column="1" Margin="5">500000</TextBox> <StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="1"> <Button Name="cmdFind" Margin="5" Padding="3" Click="cmdFind_Click">Find Primes</Button> <Button Name="cmdCancel" Margin="5" Padding="3" IsEnabled="False" Click="cmdCancel_Click">Cancel</Button> </StackPanel> <TextBlock Grid.Row="3" Margin="5">Results:</TextBlock> <ListBox Name="lstPrimes" Grid.Row="3" Grid.Column="1" Margin="5"></ListBox> <ProgressBar Name="progressBar" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Bottom" MinHeight="20" Minimum="0" Maximum="100" Height="20"></ProgressBar> </Grid> </Window>
不需要任何加锁代码,也不需要使用D1spatcher.BeginInvoke()方法。BackgroundWorker
对象自动解决了这些问题。


浙公网安备 33010602011771号