了解多线程模型
多线程是指同时执行多块代码。多线程的目标通常是用于创建能够更好地进行响应的用户界面——当执行其他工作时不会冻结的用户界面——尽管当执行需要消耗大量的CPU时间的算法时,或者长时间执行其他工作时,也可以使用多线程更好地利用多核CPU的功能。WPF支持单线程单元(Single-Thread Apartment)模型,该模型与在Windows窗体应用程序中使用的模型非常类似。它具有以下几条核心规则:
- WPF元素具有线程关联性。创建WPF元素的线程拥有所创建的元素,其他线程不能直接与这些WPF元素进行交互(元素使在窗口中显示的WPF对象)。
- 具有线程关联性的WPF对象都在类层次的某个位置继承自DispatcherObject类。DispatcherObject类提供了少量成员,用于核实为了使用特定的对象,代码是否在正确的线程上执行,并且(如果没有在正确的线程上执行)是否能切换位置。
- 实际上,线程运行整个应用程序并拥有所有WPF对象。尽管可使用单独的线程显示单独的窗口,但这种设计很少使用。
Dispatcher类
调度程序管理在WPF应用程序中发生的操作。调度程序拥有应用程序线程,并管理工作项队列。当应用程序运行时,调度程序接受新的工作请求,并一次执行一个任务。
从技术角度看,当在新程序中第一次实例化DispatcherObject类的派生类时,会创建调度程序。如果创建相互独立的线程,并用他们显示相互独立的窗口,最终将创建多个调度程序。然而,大多数应用程序都保持简单方式,并坚持使用一个用户界面线程和一个调度程序。然后,他们使用多线程多礼数据操作和其他后台任务。
注意:
调度程序时System.Windows.Threading.Dispatcher类的实例。所有与调度程序相关的对象都位于System.Windows.Threading名称空间,这时WPF新添加的一个名称空间(自.NET 1.0以来的核心线程类位于System.Threading名称空间)
可使用静态的Dispatcher.CurrentDispatcher属性检索当前线程的调度程序。使用这个Dispatcher对象,可关联事件处理程序以响应未处理的异常,或当关闭调度程序时进行响应。也可以获取调度程序控制的System.Threading.Thread的引用,关闭调度程序或将代码封送到正确的线程。
DispatcherObject类
在大多数情况下,不会直接与调度程序交互。但会花费大量时间使用DispatcherObject类的实例,因为每个WPF可视化对象都继承自这个类。DispatcherObject实例时链接到调度程序的简单对象——换句话说,时绑定到调度程序线程的对象。DispatcherObject类值提供了三个成员,如下:
- Dispatcher 属性 返回管理该对象的调度程序
- CheckAccess 方法 如果代码在城阙的线程上使用对象,就返回true,否则返回false
- VerifyAccess 方法 如果代码在正确的线程上使用对象,就什么也不做,否则抛出InvalidOperationException异常
WPF对象未保护自身会频繁调用VerifyAccess方法。但这并不时说WPF对象会调用VerifyAccess方法来响应每个操作(因为这样严重影响性能),但会足够频繁地调用该方法,从而不能在错误的线程中长时间使用一个对象。
例如,下面的代码通过创建新的System.Threading.Thread对象来响应按钮单击。然后使用创建的线程加载少量代码来改变当前窗口中的一个文本框:
private void cmdBreakRules_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(UpdateTextWrong);
thread.Start();
}
private void UpdateTextWrong()
{
Thread.Sleep(TimeSpan.FromSeconds(5));
txt.Text = "Here is some new text.";
}
上面的代码注定会失败。UpdateTextWrong方法将在新线程上执行,并且不允许这个新线程访问WPF对象。在本例中,Text Box对象通过调用VerifyAccess方法捕获这一非法操作,并抛出InvalidOperationException异常。
为改正上面的代码,需要获取拥有TextBox对象的调度程序的引用(这个调度程序也拥有应用程序中的窗口和所有其他WPF对象)。一旦访问这个调度程序就可以调用Dispatcher.Invoke方法将一些代码封送到调度程序线程。本质上,BeginInvoke方法会将代码安排为调度程序的任务。然后 调度程序会执行这些代码。下面时改正后的代码:
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方法具有两个参数。第一个参数只是任务的优先级。在大多数情况下,会使用DispatcherPriority.Normal,但如果任务不需要被立即完成,也可是使用更低的优先级,并且直到调度程序没有其他工作时才会执行该任务。例如,如果需要在用户界面中的某个地方,显示与某个长时间运行的操作相关的状态信息,这可能时合理的。可使用DispatcherPriority.ApplicationIdle等待应用程序在完成所有其他工作时执行制定的任务,或者使用更低的DispatcherPriority.SystemIdle进行等待,直到整个系统都处于休息状态,并且CPU处于空闲状态。
也可以使用比正常优先级更高的优先级,使调度程序立即关注指定的任务。但对剑为输入纤细(如按键)使用更高的优先级。这些任务需要几乎在瞬间进行处理,否则会感觉应用程序的运行时缓慢的。另一方面,为后台操作增加几毫秒的额外时间不会被注意到,所以对于这种情况 ,使用DispatcherPriority.Normal优先级更加合理。
BeginInvoke方法的第二个参数时指向一个方法的为多,该方法具有希望执行的代码。这个方法可以在代码中的其他地方定义,也可以使用匿名方法在内部定义代码(就像在这个示例中所做的那样)。对于见到那操作,使用内联方法效果较好,例如本例只需要使用一行代码跟新用户界面。然而,如果需要使用更复杂的处理过程更新用户界面,最后将将这些代码分解到单独的方法中。
注意:
BeginInvoke方法还有返回值,上面的示例美哟使用这个方悔之 。BeginInvoke方法返回一个DispatcherOperation对象,通过该对象可跟踪封送操作的状态,并确定改代码何时已实际执行完毕。人后,很少使用DispatcherOperation对象,因为传递到BeginInvoke方法的代码应当只需要很短的时间就可以执行完毕。
请记住,如果正在执行耗时的后台工作,就 需要在单独的线程中执行这个操作,然后将操作结果封送到调度程序线程(在此更新用户界面或修改共享对象)。在传递给BeginInvoke的党法中执行耗时的代码时不合理的。例如,下面稍微重新安排的代码虽然能够工作,但并不合理:
private void UpdateTextRight()
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
(ThreadStart)delegate()
{
Thread.Sleep(TimeSpan.FromSeconds(5));
txt.Text = "Here is some new text.";
};
}
这里的问题时所有工作都在调度程序线程上进行。这意味着上面的代码将以非多线程应用程序采用的方法连接倒调度程序。
注意:
调度程序还提供了Invoke方法。与BeignInvoke方法类似,Invoke方法将制定的代码封送到调度程序线程。但与BeignInvoke方法不同,Invoke方法会拖延线程知道调用程序执行您指定的代码。如果需要暂停异步操作直到用户提供一些返回信息,可使用Invoke方法。例如,可调用Invoke方法运行某个代码片段以显示具有OK/Cancel按钮的对话框。如果用户单击了按钮,而且封送的代码已经完成,Invoke方法将返回,并且可针对用户的响应执行操作。
BackgroundWorker类
BackgroundWorker组建时 .NET 2.0 版本提供的,用于简化Windows窗体应用程序中与线程相关的问题。然而,在WPF中同样使用BackgroundWorker组建。BackgroundWorker组建为在单独线程中运行耗时的任务提供了一宗非常简单的方法。它在后台使用调度程序,并使用基于事件的模型对封送问题进行抽象。BackgroundWorker组件还支持进度事件和取消消息。对于这两种情况都隐藏了线程细节,以方便代码的编写。
以下示例在给定范围内查找素数。方法为:首先得到在某个数字范围内的所有整数列表,然后剔除所有小于或等于最大数平方根的素数的倍数。
创建BackgroundWorker对象
为使用BackgroundWorker,首先要创建该类的一个实例。有两种方式可供使用(本例使用第二种方式)
- 在代码中创建BackgroundWorker对象,并用代码关联所有事件处理程序
- 在XAML中声明BackgroundWorker对象。这种方法的优点是可使用特性关联事件处理程序。但因为BackgroundWorker不是WPF的可见元素,所以不能在任意位置放置,需要作为窗口的资源声明BackgroundWorker对象。
<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>
using System;
using System.Windows;
using System.ComponentModel;
namespace Multithreading
{
public partial class BackgroundWorkerTest : System.Windows.Window
{
private System.ComponentModel.BackgroundWorker backgroundWorker;
public BackgroundWorkerTest()
{
InitializeComponent();
backgroundWorker = ((System.ComponentModel.BackgroundWorker)this.FindResource("backgroundWorker"));
}
private void cmdFind_Click(object sender, RoutedEventArgs e)
{
cmdFind.IsEnabled = false;
cmdCancel.IsEnabled = true;
lstPrimes.Items.Clear();
if (!Int32.TryParse(txtFrom.Text, out int from))
{
MessageBox.Show("Invalid From value.");
return;
}
if (!Int32.TryParse(txtTo.Text, out int to))
{
MessageBox.Show("Invalid To value.");
return;
}
FindPrimesInput input = new FindPrimesInput(from, to);
backgroundWorker.RunWorkerAsync(input);
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
FindPrimesInput input = (FindPrimesInput)e.Argument;
int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker);
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
e.Result = primes;
}
private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
MessageBox.Show("Search cancelled.");
}
else if (e.Error != null)
{
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;
}
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
}
private void cmdCancel_Click(object sender, RoutedEventArgs e)
{
backgroundWorker.CancelAsync();
}
}
}
namespace Multithreading
{
public class FindPrimesInput
{
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];
for (int i = 0; i < list.Length; i++)
{
list[i] = fromNumber;
fromNumber += 1;
}
int maxDiv = (int)System.Math.Floor(System.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 null;
}
if (backgroundWorker.WorkerReportsProgress)
{
backgroundWorker.ReportProgress(i / iteration);
}
}
}
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;
}
}
}