WPF 多线程
简介
这是一篇《WPF编程宝典》的读书笔记。
Dispatcher
调度程序(dispatcher)管理在WPF应用程序中发生的操作。调度程序拥有应用程序线程,并管理工作项队列。当应用程序运行时,调度程序接受新的工作请求,并且一次执行一个任务。
从技术角度看,当在新线程中第一次实例化DispatcherObject类的派生类时,会创建调度程序。如果创建相互独立的线程,并用他们显示相互独立的窗口,最终将创建多个调度程序。然而,大多数应用程序都保持简单方式,并坚持使用一个用户界面线程和一个调度程序。然后,它们使用多线程管理数据操作和其他后台任务。
可使用静态的Dispatcher.CurrentDispatcher
属性检索当前线程的调度程序。使用这个Dispatcher
对象,可关联事件处理程序以相应未处理的异常,或当关闭调度程序时进行相应。也可以获取调度程序控制的System.Threading.Thread的引用,关闭调度程序或将代码封送(marshal)到正确的线程。
DispatcherObject类
名称 | 说明 |
---|---|
Dispatcher | 返回管理该对象的调度程序 |
CheckAccess() | 如果代码在正确的线程上使用对象,就返回true,否则返回false |
VerifyAccess() | 如果代码在正确 的线程上使用对象,就什么也不做,否则抛出InvalidOperationException异常 |
示例:下面的代码通过创建新的System.Therading.Thread对象来响应按钮单击。然后使用创建的线程加载少量代码来改变当前窗口中的一个文本框:
如下代码注定会失败,UpdateTextWrong()方法将在新线程上执行,并且不允许这个新线程访问WPF对象。在本例中,TextBox对象通过调用VerifyAccess()方法捕获这一非法操作,并抛出InvalidOperationException异常。
//错误示范:
private void btn1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(UpdateTextWrong);
thread.Start();
}
private void UpdateTextWrong()
{
//模拟某项工作在2秒延迟的情况下进行
Thread.Sleep(TimeSpan.FromSeconds(2));
text1.Text = "你正在学习WPF! Invoke";
}
为改变上面的代码,需要获取拥有TextBox对象的调度程序的引用(这个调度程序也拥有应用程序中的窗口和所有其他WPF对象)。一旦访问这个调度程序,就可以调用Dispatcher.Invoke()方法将一些代码封送到调度程序线程。本质上BeginInvoke()方法会将代码安排为调度程序的任务,然后调度程序会执行这些代码。
//正确示范:
private void btn1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(UpdateTextWrong);
thread.Start();
}
private void UpdateTextWrong()
{
//模拟某项工作在2秒延迟的情况下进行
Thread.Sleep(TimeSpan.FromSeconds(2));
//从当前窗口获取调度程序,并使用它来调用
//更新代码
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
text1.Text = "你正在学习WPF! Invoke";
}));
}
Dispatcher.BeginInvoke()方法具有两个参数:
- 第一个参数指示任务的优先级。在大多数情况下会使用DispatcherPriority.Normal,但如果任务不需要被立即执行完成,也可以使用更低的优先级,并且指导调度程序没有其他工作时才会执行该任务。输入消息(如按键)推荐使用更高优先级,否则会感觉应用程序的运行时缓慢的。
- 第二个参数时指向一个方法的委托,该方法具有希望执行的代码。这个方法可以在代码中的其他地方定义,也可以使用匿名方法在内部定义代码。
- BeginInvoke()方法还有返回值,返回一个DispatcherOperation对象,通过该对象可跟踪封送操作的状态,并确定代码何使已实际执行完毕,然而,很少使用DispatcherOperation对象,因为传递到BeginInvoke()的方法应当只需很短的时间就可以执行完毕。
注意:如果执行耗时的后台操作,就需要在单独的线程中执行这个操作,然后将操作结果封送到调度程序线程(在此更新用户界面或修改共享对象)。在传递给BeginInvoke()的方法中执行耗时的代码是不合理的。例如,下面稍微重新安排的代码虽然能够工作,但并不合理:
private void UpdateTextWrong2()
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(5));
text1.Text = "你正在学习WPF! BeginInvoke";
}));
调度程序还提供了Invoke()方法,与BeginInvoke()方法类似,Invoke()方法将指定的代码封送到调度程序线程,但与BeginInvoke()方法不同,Invoke()方法会拖延线程指导调用程序执行您指定的代码。如果需要暂停异步操作指导用户提供一些反馈信息,可使用Invoke()方法。例如,可调用Invoke()方法运行某个代码片段以显示具有OK/Cancel按钮的对话框。如果用户单击了俺就,而且封送的代码已经完成,Invoke()方法将返回,并且可针对用户的相应执行操作
BackgroundWorker类
BackgroundWorker是.NET用于执行多线程任务的控件,它允许编程者在一个单独的线程上执行一些操作。耗时的操作(如下载和数据库事务)在不断运行时可能会导致用户界面( UI)始终处于停止响应状态。如果您需要能进行响应的用户界面,并且面临与该操作相关的连续重复,则可以使用BackgroundWorker类方便地解决问题。
如果从开始到结束只有一个异步任务在后台运行,那么使用BackgroundWorker组件是非常完美的(具有可选的进度报告和取消支持)如果还需要考虑其他事情——例如,在整个应用程序生命周期运行的异步任务,或当执行其工作时与应用程序进行通信的异步任务,就需要使用.NET的线程支持来设计自定义解决方案。
重要属性
CancellationPending
:只读属性,default值为false,执行CancelAsync方法后,值为true。表明应用程序请求了取消后台操作。获取一个值,指示应用程序是否已请求取消后台操作。通过在DoWork事件中判断CancellationPending属性可以认定是否需要取消后台操作(也就是结束线程);
IsBusy
:如果后台异步操作开始执行,值为true,否则为false。获取一个值,指示 BackgroundWorker 是否正在运行异步操作。程序中使用IsBusy属性用来确定后台操作是否正在使用中;WorkerReportProgress
:如果BackgroundWorker支持后台操作进程更新,设置值为true,default值为false。** 获取或设置一个值,该值指示BackgroundWorker能否报告进度更新。**
重要方法
RunWorkerAsync()
:开始执行后台操作,执行后台操作,激发DoWork事件ReportProgress()
:激发ProgressChanged事件CancelAsync()
: 请求取消挂起的后台操作。提交终止后台操作的请求,并将CancellationPending属性值设为true。在程序其他地方要定时检查CancellationPending属性的值,作出相应操作,比如
if (worker.CancellationPending)
{
e.Cancel = true;
}
重要事件
DoWork
:调用 RunWorkerAsync 时发生,后台耗时线程可以放在这里。ProgressChanged
:调用 ReportProgress 时发生,UI线程放在这里,类似进度条的功能。RunWorkerCompleted
:当后台操作已完成、被取消或引发异常时发生,UI线程可以放在这里,任务完成后。
不要在DoWork事件处理程序中对UI线程中的对象进行操作,操作应该放在ProgressChanged和RunWorkerCompleted的事件处理程序中。
另外还有三个重要的参数是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各属性、方法、事件的调用机制和顺序:
整个生活周期内发生了3次重要的参数传递过程:
参数传递1:此次的参数传递是将RunWorkerAsync(Object)中的Object传递到DoWork事件的DoWorkEventArgs.Argument,由于在这里只有一个参数可以传递,所以在实际应用往封装一个类,将整个实例化的类作为RunWorkerAsync的Object传递到DoWorkEventArgs.Argument;
参数传递2:此次是将程序运行进度传递给ProgressChanged事件,实际使用中往往使用给方法和事件更新进度条或者日志信息;
参数传递3:在DoWork事件结束之前,将后台线程产生的结果数据赋给DoWorkEventArgs.Result一边在RunWorkerCompleted事件中调用RunWorkerCompletedEventArgs.Result属性取得后台线程产生的结果。
另外从上图可以看到DoWork事件是在后台线程中运行的,所以在该事件中不能够操作用户界面的内容,如需要更新用户界面,可以使用ProgressChanged事件及RunWorkCompleted事件来实现。
示例程序一
XAML
<Window x:Class="BackgroundWorkerExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="300">
<StackPanel>
<ProgressBar Name="progressBar" Height="20" Width="250" Margin="10"></ProgressBar>
<TextBox Name="textBox" Width="50" Height="20" HorizontalAlignment="Center"></TextBox>
<Button Name="btnProcess" Width="100" Click="btnProcess_Click" Margin="5">Start</Button>
<Button Name="btnCancel" Width="100" Click="btnCancel_Click" Margin="5">Cancel</Button>
</StackPanel>
</Window>
C#
namespace BackgroundWorkerExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
BackgroundWorker bgworker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
bgworker.WorkerReportsProgress = true;
bgworker.WorkerSupportsCancellation = true;
bgworker.DoWork += bgworker_DoWork;
bgworker.ProgressChanged += bgworker_ProgressChanged;
bgworker.RunWorkerCompleted += bgworker_RunWorkerCompleted;
}
void bgworker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
for (int i = 1; i <= 100; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
worker.ReportProgress(i);
Thread.Sleep(100);
}
}
}
void bgworker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
textBox.Text = e.ProgressPercentage.ToString();
}
void bgworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressBar.Value = 0;
if (e.Cancelled)
{
MessageBox.Show("Background task has been canceled", "info");
}
else
{
MessageBox.Show("Background task finished", "info");
}
}
private void btnProcess_Click(object sender, RoutedEventArgs e)
{
if (!bgworker.IsBusy)
{
bgworker.RunWorkerAsync();
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
bgworker.CancelAsync();
}
}
}
演示
示例程序二
public partial class MainWindow : Window
{
BackgroundWorker worker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += (sender,e)=>
{
BackgroundWorker worker = sender as BackgroundWorker;
string str = "";
for (int i = 0; i < 100000000; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
if (i%5==0)
{
str = "支付成功";
}
else if (i%3==0)
{
str = "输入密码";
}
else if (i==74)
{
str = "支付失败";
}else
{
str = "支付中。。。";
}
worker.ReportProgress(i,str);
Thread.Sleep(100);
}
}
};
worker.ProgressChanged += (s,e)=>
{
proBar.Value = e.ProgressPercentage;
textblock.Text = e.UserState.ToString();
text.Text = proBar.Value.ToString();
};
worker.RunWorkerCompleted += (s, e) =>
{
proBar.Value = 0;
if (e.Cancelled)
{
MessageBox.Show("后台任务已被取消","信息");
}
else
{
MessageBox.Show("后台任务已经完成","信息");
}
};
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (!worker.IsBusy)
{
worker.RunWorkerAsync();
}
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
worker.CancelAsync();
}
}
文章推荐
Dispatcher:https://www.cnblogs.com/chillsrc/p/4482691.html
BackgroundWorker:https://www.jianshu.com/p/b89f39c5f803
BackgroundWorker:https://www.cnblogs.com/tom-tong/archive/2012/02/22/2363965.html